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/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
+ });