pandora-cli-skills 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README_FOR_SHARING.md +98 -0
- package/SKILL.md +121 -0
- package/cli/pandora.cjs +2000 -0
- package/package.json +53 -0
- package/references/checklist.md +24 -0
- package/references/contracts.md +26 -0
- package/references/creation-script.md +67 -0
- package/scripts/.env.example +7 -0
- package/scripts/create_market_launcher.ts +434 -0
- package/scripts/create_polymarket_clone_and_bet.ts +545 -0
- package/tsconfig.json +13 -0
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pandora-cli-skills",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Pandora CLI & Skills",
|
|
5
|
+
"main": "cli/pandora.cjs",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pandora": "cli/pandora.cjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"cli/pandora.cjs",
|
|
11
|
+
"scripts/create_market_launcher.ts",
|
|
12
|
+
"scripts/create_polymarket_clone_and_bet.ts",
|
|
13
|
+
"scripts/.env.example",
|
|
14
|
+
"references/checklist.md",
|
|
15
|
+
"references/contracts.md",
|
|
16
|
+
"references/creation-script.md",
|
|
17
|
+
"SKILL.md",
|
|
18
|
+
"README_FOR_SHARING.md",
|
|
19
|
+
"tsconfig.json"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"cli": "node cli/pandora.cjs",
|
|
23
|
+
"init-env": "node cli/pandora.cjs init-env",
|
|
24
|
+
"doctor": "node cli/pandora.cjs doctor",
|
|
25
|
+
"setup": "node cli/pandora.cjs setup",
|
|
26
|
+
"dry-run": "node cli/pandora.cjs launch --dry-run",
|
|
27
|
+
"execute": "node cli/pandora.cjs launch --execute",
|
|
28
|
+
"dry-run:clone": "node cli/pandora.cjs clone-bet --dry-run",
|
|
29
|
+
"lint": "npm run typecheck",
|
|
30
|
+
"typecheck": "npx tsc --noEmit",
|
|
31
|
+
"pack:dry-run": "npm pack --dry-run",
|
|
32
|
+
"build": "npm run typecheck",
|
|
33
|
+
"test:unit": "node --test tests/unit/sanity.test.cjs",
|
|
34
|
+
"test:cli": "node --test tests/cli/cli.integration.test.cjs",
|
|
35
|
+
"test:smoke": "node tests/smoke/pack-install-smoke.cjs",
|
|
36
|
+
"test": "npm run build && npm run test:unit && npm run test:cli && npm run test:smoke"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [],
|
|
39
|
+
"author": "",
|
|
40
|
+
"license": "ISC",
|
|
41
|
+
"type": "commonjs",
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"tsx": "^4.21.0",
|
|
47
|
+
"viem": "^2.46.2"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^25.3.0",
|
|
51
|
+
"typescript": "^5.9.3"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Pre-flight + Post-launch Checklist
|
|
2
|
+
|
|
3
|
+
## Pre-flight
|
|
4
|
+
- [ ] Wallet ETH balance > 0.02 + fees buffer
|
|
5
|
+
- [ ] USDC balance >= planned liquidity per market + 0.2% buffer
|
|
6
|
+
- [ ] Oracle/Factory addresses match runtime config
|
|
7
|
+
- [ ] `MarketFactory.oracle()` returns expected oracle
|
|
8
|
+
- [ ] Oracle fees fetched on-chain and match expected
|
|
9
|
+
- [ ] Poll impl bytecode exists (`extcodesize > 0`)
|
|
10
|
+
- [ ] Sources are reachable URLs / explicit docs pages
|
|
11
|
+
- [ ] Deadline in future and `targetTimestamp` set correctly
|
|
12
|
+
|
|
13
|
+
## Launch
|
|
14
|
+
- [ ] Poll tx includes `operatorGasFee + protocolFee` in msg.value
|
|
15
|
+
- [ ] USDC approval transaction successful
|
|
16
|
+
- [ ] Market tx mined and `MarketCreated`/`PariMutuelCreated` event captured
|
|
17
|
+
- [ ] Poll and market address saved in a ledger file
|
|
18
|
+
- [ ] Both tx hashes copied and verified on block explorer
|
|
19
|
+
|
|
20
|
+
## Post-launch
|
|
21
|
+
- [ ] Market appears in Pandora `useMarkets` feed
|
|
22
|
+
- [ ] Poll/status UI resolves to correct question/rules/sources
|
|
23
|
+
- [ ] Initial liquidity reflected in TVL
|
|
24
|
+
- [ ] Publish markdown/copy drafted with category + angle + hooks
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Pandora Market Contracts Reference (Provided Mainnet Deployment)
|
|
2
|
+
|
|
3
|
+
Use these addresses exactly as source-of-truth unless reconfigured in runtime:
|
|
4
|
+
|
|
5
|
+
- Deployer: `0x972405d0009DdD8906a36109B069E4D7d02E5801`
|
|
6
|
+
- PredictionOracle: `0x259308E7d8557e4Ba192De1aB8Cf7e0E21896442`
|
|
7
|
+
- PredictionPoll implementation: `0xC49c177736107fD8351ed6564136B9ADbE5B1eC3`
|
|
8
|
+
- MarketFactory: `0xaB120F1FD31FB1EC39893B75d80a3822b1Cd8d0c`
|
|
9
|
+
- Operator gas fee: `0.000106 ETH`
|
|
10
|
+
- Protocol fee: `0.0001 ETH`
|
|
11
|
+
- Total per poll: `0.000206 ETH`
|
|
12
|
+
|
|
13
|
+
Market implementation pointers:
|
|
14
|
+
|
|
15
|
+
- OutcomeToken: `0x15AF9A6cE764a7D2b6913e09494350893436Ab3d`
|
|
16
|
+
- PredictionAMM: `0x7D45D4835001347B31B722Fb830fc1D9336F09f4`
|
|
17
|
+
- PredictionPariMutuel: `0x5CaF2D85f17A8f3b57918d54c8B138Cacac014BD`
|
|
18
|
+
|
|
19
|
+
Collateral:
|
|
20
|
+
- USDC: `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48`
|
|
21
|
+
- Platform treasury: `0x8789F22a0456FEddaf9074FF4cEE55E4122095f0`
|
|
22
|
+
|
|
23
|
+
Notes:
|
|
24
|
+
- Use the exact addresses in scripts before any new market creation.
|
|
25
|
+
- Current epoch shown by user: `5902449n` (for scheduling math only, not hardcoded in contracts).
|
|
26
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Creator Script Guide
|
|
2
|
+
|
|
3
|
+
The skill includes `scripts/create_market_launcher.ts`, which performs:
|
|
4
|
+
|
|
5
|
+
1. Reads deployer balances (native + USDC)
|
|
6
|
+
2. Fetches Oracle fees and computes required poll fee
|
|
7
|
+
3. Calls `PredictionOracle.createPoll(...)` with fee value
|
|
8
|
+
4. Waits for `PollCreated` and extracts poll address
|
|
9
|
+
5. Approves USDC for `MarketFactory` when needed
|
|
10
|
+
6. Calls:
|
|
11
|
+
- `createMarket(...)` for AMM
|
|
12
|
+
- `createPariMutuel(...)` for PariMutuel
|
|
13
|
+
7. Prints transaction hashes and created market artifacts
|
|
14
|
+
|
|
15
|
+
## Core CLI options
|
|
16
|
+
|
|
17
|
+
- `--question` (required): market question
|
|
18
|
+
- `--rules` (required): must include explicit Yes/No + edge-case handling
|
|
19
|
+
- `--sources` (required, repeatable): at least 2 public `http/https` URLs
|
|
20
|
+
- `--arbiter` (optional): defaults to whitelisted arbiter
|
|
21
|
+
- `--deadline-epoch` or `--target-timestamp` (required): unix timestamp in seconds
|
|
22
|
+
- `--target-timestamp-offset-hours` (optional): default `1`
|
|
23
|
+
- `--category` (optional): category id (default `0`)
|
|
24
|
+
- `--market-type` (required): `amm` or `parimutuel`
|
|
25
|
+
- `--liquidity` (required): initial liquidity in USDC (**minimum 10 USDC**)
|
|
26
|
+
- `--distribution-yes` / `--distribution-no`: distribution hint (1e9 scale, must sum to `1000000000`)
|
|
27
|
+
|
|
28
|
+
AMM-only:
|
|
29
|
+
- `--fee-tier`: `500`, `3000`, `10000` (default `3000`)
|
|
30
|
+
- `--max-imbalance`: `maxPriceImbalancePerHour` (default `10000`)
|
|
31
|
+
|
|
32
|
+
PariMutuel-only:
|
|
33
|
+
- `--curve-flattener` (default `7`)
|
|
34
|
+
- `--curve-offset` (default `30000`)
|
|
35
|
+
|
|
36
|
+
## Core execution
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm run init-env
|
|
40
|
+
# edit scripts/.env values
|
|
41
|
+
npm run doctor
|
|
42
|
+
|
|
43
|
+
pandora launch \
|
|
44
|
+
--dry-run \
|
|
45
|
+
--question "Will BTC close above $100k by end of 2026?" \
|
|
46
|
+
--rules "Resolves YES if BTC/USD closes above 100000 on 2026-12-31 per listed public sources. Resolves NO otherwise. If cancelled, postponed, abandoned, or unresolved by 2027-01-02, resolves NO." \
|
|
47
|
+
--sources "https://coinmarketcap.com/currencies/bitcoin/" "https://www.coingecko.com/en/coins/bitcoin" \
|
|
48
|
+
--target-timestamp 1798675200 \
|
|
49
|
+
--target-timestamp-offset-hours 1 \
|
|
50
|
+
--arbiter 0x818457C9e2b18D87981CCB09b75AE183D107b257 \
|
|
51
|
+
--category 3 \
|
|
52
|
+
--market-type amm \
|
|
53
|
+
--liquidity 100 \
|
|
54
|
+
--fee-tier 3000 \
|
|
55
|
+
--distribution-yes 600000000 \
|
|
56
|
+
--distribution-no 400000000
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
If `pandora` is not linked yet, use `node cli/pandora.cjs launch ...`.
|
|
60
|
+
|
|
61
|
+
Execution example (live):
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pandora launch --execute --market-type parimutuel --liquidity 250 ...
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Use `--dry-run` before `--execute` on first run.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Runtime variables for market launcher scripts
|
|
2
|
+
CHAIN_ID=1
|
|
3
|
+
RPC_URL=https://ethereum.publicnode.com
|
|
4
|
+
PRIVATE_KEY=0x...
|
|
5
|
+
ORACLE=0x259308E7d8557e4Ba192De1aB8Cf7e0E21896442
|
|
6
|
+
FACTORY=0xaB120F1FD31FB1EC39893B75d80a3822b1Cd8d0c
|
|
7
|
+
USDC=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
import {
|
|
4
|
+
createPublicClient,
|
|
5
|
+
createWalletClient,
|
|
6
|
+
decodeEventLog,
|
|
7
|
+
formatUnits,
|
|
8
|
+
http,
|
|
9
|
+
parseUnits,
|
|
10
|
+
type Address,
|
|
11
|
+
} from 'viem';
|
|
12
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_ARBITER = '0x818457C9e2b18D87981CCB09b75AE183D107b257';
|
|
15
|
+
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
16
|
+
const MIN_SOURCE_COUNT = 2;
|
|
17
|
+
const MIN_DEADLINE_WINDOW_SECONDS = 12 * 60 * 60;
|
|
18
|
+
|
|
19
|
+
type ParsedArgs = Record<string, string | string[]>;
|
|
20
|
+
|
|
21
|
+
const parseArgs = (argv: string[]): { args: ParsedArgs; flags: Set<string> } => {
|
|
22
|
+
const args: ParsedArgs = {};
|
|
23
|
+
const flags = new Set<string>();
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
26
|
+
const token = argv[i];
|
|
27
|
+
if (!token.startsWith('--')) continue;
|
|
28
|
+
|
|
29
|
+
const key = token.replace(/^--/, '');
|
|
30
|
+
if (key === 'dry-run' || key === 'execute') {
|
|
31
|
+
flags.add(key);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (key === 'sources') {
|
|
36
|
+
const values: string[] = [];
|
|
37
|
+
let j = i + 1;
|
|
38
|
+
while (j < argv.length && !argv[j].startsWith('--')) {
|
|
39
|
+
values.push(argv[j]);
|
|
40
|
+
j += 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!values.length) {
|
|
44
|
+
throw new Error('Missing value for --sources');
|
|
45
|
+
}
|
|
46
|
+
args[key] = values;
|
|
47
|
+
i = j - 1;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const next = argv[i + 1];
|
|
52
|
+
if (!next || next.startsWith('--')) {
|
|
53
|
+
throw new Error(`Missing value for --${key}`);
|
|
54
|
+
}
|
|
55
|
+
args[key] = next;
|
|
56
|
+
i += 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { args, flags };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const isValidPublicSourceUrl = (value: string): boolean => {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = new URL(value);
|
|
65
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) return false;
|
|
66
|
+
const host = parsed.hostname.toLowerCase();
|
|
67
|
+
if (['localhost', '127.0.0.1', '0.0.0.0'].includes(host)) return false;
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const hasExplicitYesNoAndEdgeCases = (rules: string): boolean => {
|
|
75
|
+
const hasYes = /\byes\b/i.test(rules);
|
|
76
|
+
const hasNo = /\bno\b/i.test(rules);
|
|
77
|
+
const hasEdgeCase = /(cancel|canceled|cancelled|postpone|postponed|abandon|abandoned|void|refund|reschedul|replay|unresolved)/i.test(rules);
|
|
78
|
+
return hasYes && hasNo && hasEdgeCase;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const toInt = (value: string, fallback: number): number => {
|
|
82
|
+
const parsed = Number(value);
|
|
83
|
+
if (!Number.isInteger(parsed)) return fallback;
|
|
84
|
+
return parsed;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const toNumber = (value: string, fallback: number): number => {
|
|
88
|
+
const parsed = Number(value);
|
|
89
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
90
|
+
return parsed;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const ERC20_ABI = [
|
|
94
|
+
{ type: 'function', name: 'approve', stateMutability: 'nonpayable', inputs: [{ name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }], outputs: [{ type: 'bool' }] },
|
|
95
|
+
{ type: 'function', name: 'allowance', stateMutability: 'view', inputs: [{ name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }], outputs: [{ type: 'uint256' }] },
|
|
96
|
+
{ type: 'function', name: 'balanceOf', stateMutability: 'view', inputs: [{ name: 'account', type: 'address' }], outputs: [{ type: 'uint256' }] },
|
|
97
|
+
] as const;
|
|
98
|
+
|
|
99
|
+
const ORACLE_ABI = [
|
|
100
|
+
{ name: 'operatorGasFee', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
|
|
101
|
+
{ name: 'protocolFee', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
|
|
102
|
+
{
|
|
103
|
+
name: 'createPoll',
|
|
104
|
+
type: 'function',
|
|
105
|
+
stateMutability: 'payable',
|
|
106
|
+
inputs: [
|
|
107
|
+
{ name: '_question', type: 'string' },
|
|
108
|
+
{ name: '_rules', type: 'string' },
|
|
109
|
+
{ name: '_sources', type: 'string[]' },
|
|
110
|
+
{ name: '_targetTimestamp', type: 'uint256' },
|
|
111
|
+
{ name: '_arbiter', type: 'address' },
|
|
112
|
+
{ name: '_category', type: 'uint8' },
|
|
113
|
+
],
|
|
114
|
+
outputs: [{ name: 'pollAddress', type: 'address' }],
|
|
115
|
+
},
|
|
116
|
+
] as const;
|
|
117
|
+
|
|
118
|
+
const FACTORY_ABI = [
|
|
119
|
+
{
|
|
120
|
+
name: 'createMarket',
|
|
121
|
+
type: 'function',
|
|
122
|
+
stateMutability: 'nonpayable',
|
|
123
|
+
inputs: [
|
|
124
|
+
{ name: '_pollAddress', type: 'address' },
|
|
125
|
+
{ name: '_collateral', type: 'address' },
|
|
126
|
+
{ name: '_initialLiquidity', type: 'uint256' },
|
|
127
|
+
{ name: '_distributionHint', type: 'uint256[2]' },
|
|
128
|
+
{ name: '_feeTier', type: 'uint24' },
|
|
129
|
+
{ name: '_maxPriceImbalancePerHour', type: 'uint24' },
|
|
130
|
+
],
|
|
131
|
+
outputs: [{ name: 'marketAddress', type: 'address' }],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'createPariMutuel',
|
|
135
|
+
type: 'function',
|
|
136
|
+
stateMutability: 'nonpayable',
|
|
137
|
+
inputs: [
|
|
138
|
+
{ name: '_pollAddress', type: 'address' },
|
|
139
|
+
{ name: '_collateral', type: 'address' },
|
|
140
|
+
{ name: '_initialLiquidity', type: 'uint256' },
|
|
141
|
+
{ name: '_distributionHint', type: 'uint256[2]' },
|
|
142
|
+
{ name: '_curveFlattener', type: 'uint8' },
|
|
143
|
+
{ name: '_curveOffset', type: 'uint24' },
|
|
144
|
+
],
|
|
145
|
+
outputs: [{ name: 'marketAddress', type: 'address' }],
|
|
146
|
+
},
|
|
147
|
+
] as const;
|
|
148
|
+
|
|
149
|
+
const PollCreatedEvent = {
|
|
150
|
+
type: 'event',
|
|
151
|
+
name: 'PollCreated',
|
|
152
|
+
inputs: [
|
|
153
|
+
{ indexed: true, name: 'pollAddress', type: 'address' },
|
|
154
|
+
{ indexed: true, name: 'creator', type: 'address' },
|
|
155
|
+
{ indexed: false, name: 'deadlineEpoch', type: 'uint32' },
|
|
156
|
+
{ indexed: false, name: 'question', type: 'string' },
|
|
157
|
+
],
|
|
158
|
+
} as const;
|
|
159
|
+
|
|
160
|
+
const RAW_ARGS = process.argv.slice(2);
|
|
161
|
+
const { args: parsedArgs, flags } = parseArgs(RAW_ARGS);
|
|
162
|
+
const arg = (name: string, fallback = ''): string => {
|
|
163
|
+
const value = parsedArgs[name];
|
|
164
|
+
return typeof value === 'string' ? value : fallback;
|
|
165
|
+
};
|
|
166
|
+
const listArg = (name: string): string[] => {
|
|
167
|
+
const value = parsedArgs[name];
|
|
168
|
+
return Array.isArray(value) ? value : [];
|
|
169
|
+
};
|
|
170
|
+
const hasFlag = (name: string) => flags.has(name);
|
|
171
|
+
|
|
172
|
+
const marketTypeArg = arg('market-type', 'amm').toLowerCase();
|
|
173
|
+
const marketType = marketTypeArg === 'parimutuel' || marketTypeArg === 'pari' || marketTypeArg === 'pm' ? 'parimutuel' : 'amm';
|
|
174
|
+
|
|
175
|
+
const args = {
|
|
176
|
+
dryRun: hasFlag('dry-run'),
|
|
177
|
+
execute: hasFlag('execute'),
|
|
178
|
+
question: arg('question'),
|
|
179
|
+
rules: arg('rules'),
|
|
180
|
+
sources: listArg('sources'),
|
|
181
|
+
targetTimestamp: arg('target-timestamp') || arg('deadline-epoch'),
|
|
182
|
+
targetTimestampOffsetHours: arg('target-timestamp-offset-hours', '1'),
|
|
183
|
+
arbiter: (arg('arbiter', DEFAULT_ARBITER) as Address),
|
|
184
|
+
category: toInt(arg('category', '0'), 0),
|
|
185
|
+
marketType,
|
|
186
|
+
liquidity: arg('liquidity', '0'),
|
|
187
|
+
distributionYes: arg('distribution-yes', '500000000'),
|
|
188
|
+
distributionNo: arg('distribution-no', '500000000'),
|
|
189
|
+
feeTier: toInt(arg('fee-tier', '3000'), 3000),
|
|
190
|
+
maxImbalance: toInt(arg('max-imbalance', '10000'), 10000),
|
|
191
|
+
curveFlattener: toInt(arg('curve-flattener', '7'), 7),
|
|
192
|
+
curveOffset: toInt(arg('curve-offset', '30000'), 30000),
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
if (!args.question || !args.rules) {
|
|
196
|
+
console.error('Missing required args: --question --rules');
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!args.sources.length) {
|
|
201
|
+
console.error('Missing required args: at least two --sources values are required');
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (args.sources.length < MIN_SOURCE_COUNT) {
|
|
206
|
+
console.error(`Provide at least ${MIN_SOURCE_COUNT} public --sources URLs`);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const invalidSources = args.sources.filter((source) => !isValidPublicSourceUrl(source));
|
|
211
|
+
if (invalidSources.length) {
|
|
212
|
+
console.error('Invalid --sources values. Use only public http/https URLs:', invalidSources.join(', '));
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!hasExplicitYesNoAndEdgeCases(args.rules)) {
|
|
217
|
+
console.error('Rules must include explicit Yes/No outcomes and edge-case handling (cancel/postpone/abandoned/unresolved cases).');
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!args.dryRun && !args.execute) {
|
|
222
|
+
console.error('You must pass either --dry-run or --execute');
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (args.arbiter.toLowerCase() === ZERO_ADDRESS) {
|
|
227
|
+
console.error('Invalid --arbiter. Zero address is not allowed.');
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(args.arbiter)) {
|
|
232
|
+
console.error('Invalid --arbiter address format.');
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (args.marketType === 'amm' && ![500, 3000, 10000].includes(args.feeTier)) {
|
|
237
|
+
console.error('Invalid --fee-tier for AMM. Allowed values: 500 | 3000 | 10000');
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (args.marketType === 'parimutuel') {
|
|
242
|
+
if (args.curveFlattener < 1 || args.curveFlattener > 11) {
|
|
243
|
+
console.error('Invalid --curve-flattener for PariMutuel. Allowed range: 1-11');
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const liquidityAmount = toNumber(args.liquidity, Number.NaN);
|
|
249
|
+
if (!Number.isFinite(liquidityAmount) || liquidityAmount < 10) {
|
|
250
|
+
console.error('Invalid --liquidity. Must be at least 10 USDC');
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const distributionSum = Number(args.distributionYes) + Number(args.distributionNo);
|
|
255
|
+
if (distributionSum !== 1_000_000_000) {
|
|
256
|
+
console.error('distribution-yes + distribution-no must equal 1000000000');
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!args.targetTimestamp) {
|
|
261
|
+
console.error('Missing required args: --target-timestamp or --deadline-epoch (unix timestamp in seconds)');
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const targetTimestampSeconds = toInt(args.targetTimestamp, 0);
|
|
266
|
+
if (targetTimestampSeconds <= 0) {
|
|
267
|
+
console.error('Invalid --target-timestamp. Provide a unix timestamp in seconds.');
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const targetTimestampOffsetHours = toInt(args.targetTimestampOffsetHours, Number.NaN);
|
|
272
|
+
if (!Number.isFinite(targetTimestampOffsetHours) || targetTimestampOffsetHours < 0) {
|
|
273
|
+
console.error('Invalid --target-timestamp-offset-hours. Use a non-negative integer.');
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const targetTimestamp = BigInt(targetTimestampSeconds);
|
|
278
|
+
const targetTimestampWithOffset = targetTimestamp + BigInt(targetTimestampOffsetHours * 60 * 60);
|
|
279
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
280
|
+
if (targetTimestamp <= BigInt(nowSec)) {
|
|
281
|
+
console.error('Target deadline must be in the future.');
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
if (targetTimestampWithOffset <= BigInt(nowSec)) {
|
|
285
|
+
console.error('Effective target deadline with +offset must be in the future.');
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
if (targetTimestampWithOffset <= BigInt(nowSec + MIN_DEADLINE_WINDOW_SECONDS)) {
|
|
289
|
+
console.warn('Warning: effective target deadline is within 12h; DAO verification may be less favorable.');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const ORACLE = (process.env.ORACLE as Address) || '0x259308E7d8557e4Ba192De1aB8Cf7e0E21896442';
|
|
293
|
+
const FACTORY = (process.env.FACTORY as Address) || '0xaB120F1FD31FB1EC39893B75d80a3822b1Cd8d0c';
|
|
294
|
+
const USDC = (process.env.USDC as Address) || '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
|
|
295
|
+
const CHAIN_ID = Number(process.env.CHAIN_ID || '1');
|
|
296
|
+
const DEFAULT_RPC_BY_CHAIN_ID: Record<number, string> = {
|
|
297
|
+
1: 'https://ethereum.publicnode.com',
|
|
298
|
+
146: 'https://rpc.soniclabs.com',
|
|
299
|
+
};
|
|
300
|
+
if (!DEFAULT_RPC_BY_CHAIN_ID[CHAIN_ID]) {
|
|
301
|
+
console.error(`Unsupported CHAIN_ID=${CHAIN_ID}. Supported: 1 or 146`);
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
const RPC_URL = process.env.RPC_URL || DEFAULT_RPC_BY_CHAIN_ID[CHAIN_ID];
|
|
305
|
+
const PRIVATE_KEY = process.env.PRIVATE_KEY || process.env.DEPLOYER_PRIVATE_KEY;
|
|
306
|
+
if (!PRIVATE_KEY) {
|
|
307
|
+
console.error('Set PRIVATE_KEY');
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`);
|
|
312
|
+
const chain = CHAIN_ID === 1
|
|
313
|
+
? {
|
|
314
|
+
id: 1,
|
|
315
|
+
name: 'Ethereum',
|
|
316
|
+
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
|
|
317
|
+
rpcUrls: { default: { http: [RPC_URL] }, public: { http: [RPC_URL] } },
|
|
318
|
+
blockExplorers: { default: { name: 'Etherscan', url: 'https://etherscan.io' } },
|
|
319
|
+
}
|
|
320
|
+
: {
|
|
321
|
+
id: 146,
|
|
322
|
+
name: 'Sonic',
|
|
323
|
+
nativeCurrency: { name: 'Sonic', symbol: 'S', decimals: 18 },
|
|
324
|
+
rpcUrls: { default: { http: [RPC_URL] }, public: { http: [RPC_URL] } },
|
|
325
|
+
blockExplorers: { default: { name: 'SonicScan', url: 'https://sonicscan.org' } },
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const publicClient = createPublicClient({ chain, transport: http(RPC_URL) });
|
|
329
|
+
const walletClient = createWalletClient({ account, chain, transport: http(RPC_URL) });
|
|
330
|
+
|
|
331
|
+
async function main() {
|
|
332
|
+
console.log('Network RPC:', RPC_URL);
|
|
333
|
+
console.log('Deployer:', account.address);
|
|
334
|
+
|
|
335
|
+
const [operatorGasFee, protocolFee] = await Promise.all([
|
|
336
|
+
publicClient.readContract({ address: ORACLE, abi: ORACLE_ABI as any, functionName: 'operatorGasFee' }),
|
|
337
|
+
publicClient.readContract({ address: ORACLE, abi: ORACLE_ABI as any, functionName: 'protocolFee' }),
|
|
338
|
+
]);
|
|
339
|
+
|
|
340
|
+
const requiredFee = (operatorGasFee + protocolFee) as bigint;
|
|
341
|
+
console.log('required poll fee:', formatUnits(requiredFee, 18), 'ETH-equivalent units');
|
|
342
|
+
|
|
343
|
+
const initialLiquidity = parseUnits(args.liquidity, 6);
|
|
344
|
+
const distributionHint = [BigInt(args.distributionYes), BigInt(args.distributionNo)] as [bigint, bigint];
|
|
345
|
+
|
|
346
|
+
if (args.dryRun && !args.execute) {
|
|
347
|
+
console.log('DRY RUN: would execute this market setup:');
|
|
348
|
+
console.log({
|
|
349
|
+
marketType: args.marketType,
|
|
350
|
+
question: args.question,
|
|
351
|
+
arbiter: args.arbiter,
|
|
352
|
+
category: args.category,
|
|
353
|
+
targetTimestampProvided: String(targetTimestamp),
|
|
354
|
+
targetTimestampOnChain: String(targetTimestampWithOffset),
|
|
355
|
+
targetTimestampOffsetHours,
|
|
356
|
+
liquidityUSDC: args.liquidity,
|
|
357
|
+
distributionHint,
|
|
358
|
+
feeTier: args.marketType === 'amm' ? args.feeTier : undefined,
|
|
359
|
+
maxPriceImbalancePerHour: args.marketType === 'amm' ? args.maxImbalance : undefined,
|
|
360
|
+
curveFlattener: args.marketType === 'parimutuel' ? args.curveFlattener : undefined,
|
|
361
|
+
curveOffset: args.marketType === 'parimutuel' ? args.curveOffset : undefined,
|
|
362
|
+
requiredFeeWei: String(requiredFee),
|
|
363
|
+
oracle: ORACLE,
|
|
364
|
+
factory: FACTORY,
|
|
365
|
+
collateral: USDC,
|
|
366
|
+
});
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const hash = await walletClient.writeContract({
|
|
371
|
+
address: ORACLE,
|
|
372
|
+
abi: ORACLE_ABI as any,
|
|
373
|
+
functionName: 'createPoll',
|
|
374
|
+
args: [
|
|
375
|
+
args.question,
|
|
376
|
+
args.rules,
|
|
377
|
+
args.sources,
|
|
378
|
+
targetTimestampWithOffset,
|
|
379
|
+
args.arbiter as Address,
|
|
380
|
+
args.category,
|
|
381
|
+
],
|
|
382
|
+
value: requiredFee,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
386
|
+
const pollLog = receipt.logs.find((log) => log.address.toLowerCase() === ORACLE.toLowerCase());
|
|
387
|
+
if (!pollLog) throw new Error('No logs found for poll creation');
|
|
388
|
+
|
|
389
|
+
const parsed = decodeEventLog({
|
|
390
|
+
abi: [PollCreatedEvent],
|
|
391
|
+
data: pollLog.data,
|
|
392
|
+
topics: pollLog.topics as any,
|
|
393
|
+
}) as any;
|
|
394
|
+
const pollAddress = parsed.args.pollAddress as Address;
|
|
395
|
+
console.log('Poll created:', pollAddress);
|
|
396
|
+
|
|
397
|
+
const allowance = (await publicClient.readContract({
|
|
398
|
+
address: USDC,
|
|
399
|
+
abi: ERC20_ABI as any,
|
|
400
|
+
functionName: 'allowance',
|
|
401
|
+
args: [account.address, FACTORY],
|
|
402
|
+
})) as bigint;
|
|
403
|
+
|
|
404
|
+
if (allowance < initialLiquidity) {
|
|
405
|
+
const approveHash = await walletClient.writeContract({
|
|
406
|
+
address: USDC,
|
|
407
|
+
abi: ERC20_ABI as any,
|
|
408
|
+
functionName: 'approve',
|
|
409
|
+
args: [FACTORY, initialLiquidity],
|
|
410
|
+
});
|
|
411
|
+
await publicClient.waitForTransactionReceipt({ hash: approveHash });
|
|
412
|
+
console.log('USDC approved for factory');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const marketFn = args.marketType === 'parimutuel' ? 'createPariMutuel' : 'createMarket';
|
|
416
|
+
const marketArgs = args.marketType === 'parimutuel'
|
|
417
|
+
? [pollAddress, USDC, initialLiquidity, distributionHint, args.curveFlattener, args.curveOffset]
|
|
418
|
+
: [pollAddress, USDC, initialLiquidity, distributionHint, args.feeTier, args.maxImbalance];
|
|
419
|
+
|
|
420
|
+
const marketHash = await walletClient.writeContract({
|
|
421
|
+
address: FACTORY,
|
|
422
|
+
abi: FACTORY_ABI as any,
|
|
423
|
+
functionName: marketFn as any,
|
|
424
|
+
args: marketArgs,
|
|
425
|
+
});
|
|
426
|
+
const marketReceipt = await publicClient.waitForTransactionReceipt({ hash: marketHash });
|
|
427
|
+
console.log('Market created tx:', marketReceipt.transactionHash);
|
|
428
|
+
console.log('Done');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
main().catch((err) => {
|
|
432
|
+
console.error('Error:', err);
|
|
433
|
+
process.exit(1);
|
|
434
|
+
});
|