pandora-cli-skills 1.1.46 → 1.1.48
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.md +1 -0
- package/README_FOR_SHARING.md +5 -2
- package/SKILL.md +7 -4
- package/cli/lib/mirror_command_service.cjs +1 -1
- package/cli/lib/mirror_handlers/browse.cjs +2 -2
- package/cli/lib/mirror_service.cjs +1 -0
- package/cli/lib/parsers/mirror_remaining_flags.cjs +29 -0
- package/cli/lib/polymarket_trade_adapter.cjs +167 -12
- package/cli/lib/schema_command_service.cjs +23 -0
- package/cli/lib/trade_command_service.cjs +3 -0
- package/cli/lib/trade_market_type_service.cjs +203 -0
- package/cli/pandora.cjs +34 -19
- package/package.json +1 -1
- package/tests/cli/cli.integration.test.cjs +99 -0
- package/tests/unit/new-features.test.cjs +118 -0
- package/tests/unit/trade_market_type_service.test.cjs +109 -0
package/README.md
CHANGED
|
@@ -122,6 +122,7 @@ pandora --output json lifecycle resolve --id <lifecycle-id> --confirm
|
|
|
122
122
|
- `pandora odds record|history`
|
|
123
123
|
- `pandora autopilot run|once`
|
|
124
124
|
- `pandora mirror browse|plan|deploy|verify|lp-explain|hedge-calc|simulate|go|sync|status|close`
|
|
125
|
+
- `mirror browse` supports `--polymarket-tag-id|--polymarket-tag-ids` (aliases `--sport-tag-id|--sport-tag-ids`) for sports-tagged Gamma event discovery.
|
|
125
126
|
- `pandora polymarket check|approve|preflight|trade`
|
|
126
127
|
- `pandora resolve`
|
|
127
128
|
- `pandora lp add|remove|positions`
|
package/README_FOR_SHARING.md
CHANGED
|
@@ -200,6 +200,7 @@ Mirror advanced flags (for operator tuning):
|
|
|
200
200
|
- `--sync-interval-ms <ms>` on `mirror go` to control auto-sync tick cadence.
|
|
201
201
|
- `--oracle <address>` / `--factory <address>` on `mirror deploy` and `mirror go` for explicit contract overrides.
|
|
202
202
|
- `--polymarket-gamma-mock-url <url>` on `mirror browse|plan|verify|go|sync|status` for deterministic mock-source testing.
|
|
203
|
+
- `--polymarket-tag-id <id>` / `--polymarket-tag-ids <csv>` on `mirror browse` (aliases: `--sport-tag-id`, `--sport-tag-ids`) to query sports-tagged Gamma events.
|
|
203
204
|
- `--no-stream` on `mirror sync` to disable per-tick stdout line streaming in run mode.
|
|
204
205
|
- `--pid-file <path>` on `mirror sync stop|status` for explicit daemon process selection.
|
|
205
206
|
|
|
@@ -219,7 +220,7 @@ Mirror advanced flags (for operator tuning):
|
|
|
219
220
|
- `pandora export --wallet <0x...> --format csv --out ./trades.csv`
|
|
220
221
|
- `pandora arbitrage --venues pandora,polymarket --min-spread-pct 2 --cross-venue-only --with-rules --include-similarity`
|
|
221
222
|
- `pandora autopilot once --market-address <0x...> --side no --amount-usdc 10 --trigger-yes-below 15 --paper`
|
|
222
|
-
- `pandora mirror browse --min-yes-pct 20 --max-yes-pct 80 --min-volume-24h 100000 --limit 10`
|
|
223
|
+
- `pandora mirror browse --polymarket-tag-id 82 --min-yes-pct 20 --max-yes-pct 80 --min-volume-24h 100000 --limit 10`
|
|
223
224
|
- `pandora mirror plan --source polymarket --polymarket-market-id <id> --with-rules --include-similarity`
|
|
224
225
|
- `pandora mirror go --polymarket-slug <slug> --liquidity-usdc 10 --paper`
|
|
225
226
|
- `pandora mirror verify --pandora-market-address <0x...> --polymarket-market-id <id> --include-similarity`
|
|
@@ -262,7 +263,9 @@ Mirror advanced flags (for operator tuning):
|
|
|
262
263
|
- envelope is `ok=true`, `command="trade"`, with tx metadata (`approveTxHash` optional, `buyTxHash` required on success) plus `selectedProbabilityPct` and `riskGuards`.
|
|
263
264
|
|
|
264
265
|
## Phase 2 limitations
|
|
265
|
-
- `trade`
|
|
266
|
+
- `trade` auto-detects market type and uses the correct buy signature:
|
|
267
|
+
- PariMutuel: `buy(bool,uint256,uint256)`
|
|
268
|
+
- AMM: `buy(bool,uint256,uint256,uint256)` (deadline-aware)
|
|
266
269
|
- `minSharesOut` protection defaults to raw `0` unless explicitly set with `--min-shares-out-raw`.
|
|
267
270
|
- If indexer odds are unavailable, `quote` still returns a structured payload with `quoteAvailable=false`.
|
|
268
271
|
- `trade --execute` blocks unquoted execution by default unless `--min-shares-out-raw` or `--allow-unquoted-execute` is provided.
|
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pandora-cli-skills
|
|
3
3
|
summary: Canonical skill and operator guide for Pandora CLI including mirror, polymarket, resolve, and LP flows.
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.48
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Pandora CLI & Skills
|
|
@@ -317,7 +317,7 @@ pandora --output json history --wallet <0x...> --limit 50
|
|
|
317
317
|
pandora --output json export --wallet <0x...> --format csv --out ./trades.csv
|
|
318
318
|
pandora --output json arbitrage --venues pandora,polymarket --min-spread-pct 3 --cross-venue-only --with-rules --include-similarity
|
|
319
319
|
pandora --output json autopilot once --market-address <0x...> --side no --amount-usdc 10 --trigger-yes-below 15 --paper
|
|
320
|
-
pandora --output json mirror browse --min-yes-pct 20 --max-yes-pct 80 --min-volume-24h 100000 --limit 10
|
|
320
|
+
pandora --output json mirror browse --polymarket-tag-id 82 --min-yes-pct 20 --max-yes-pct 80 --min-volume-24h 100000 --limit 10
|
|
321
321
|
pandora --output json mirror plan --source polymarket --polymarket-market-id <id> --with-rules --include-similarity
|
|
322
322
|
pandora --output json mirror lp-explain --liquidity-usdc 10000 --source-yes-pct 58
|
|
323
323
|
pandora --output json mirror hedge-calc --reserve-yes-usdc 8 --reserve-no-usdc 12 --excess-no-usdc 2 --polymarket-yes-pct 60
|
|
@@ -359,7 +359,9 @@ pandora --output json schema
|
|
|
359
359
|
- envelope is `ok=true`, `command="trade"`, with tx metadata (`approveTxHash` optional, `buyTxHash` required on success) plus `selectedProbabilityPct` and `riskGuards`.
|
|
360
360
|
|
|
361
361
|
## Phase 2 limitations
|
|
362
|
-
- `trade`
|
|
362
|
+
- `trade` auto-detects market type and uses the correct buy signature:
|
|
363
|
+
- PariMutuel: `buy(bool,uint256,uint256)`
|
|
364
|
+
- AMM: `buy(bool,uint256,uint256,uint256)` (deadline-aware)
|
|
363
365
|
- `--min-shares-out-raw` is the explicit slippage guard input for on-chain execution.
|
|
364
366
|
- If indexer odds are unavailable, `quote` still returns structured output with `quoteAvailable=false`.
|
|
365
367
|
- `trade --execute` blocks unquoted execution by default unless `--min-shares-out-raw` or `--allow-unquoted-execute` is provided.
|
|
@@ -560,7 +562,8 @@ Error envelope:
|
|
|
560
562
|
- `{ ok: true, command: "autopilot", data: { schemaVersion, generatedAt, strategyHash, mode, executeLive, stateFile, killSwitchFile, iterationsRequested, iterationsCompleted, stoppedReason?, parameters: { marketAddress, side, amountUsdc, triggerYesBelow?, triggerYesAbove?, intervalMs, cooldownMs, maxAmountUsdc?, maxOpenExposureUsdc, dailySpendCapUsdc, maxTradesPerDay }, state, actionCount, actions[], snapshots[], webhookReports[] } }`
|
|
561
563
|
- `mirror browse`:
|
|
562
564
|
- `{ ok: true, command: "mirror.browse", data: { schemaVersion, generatedAt, source, gammaApiError, filters, count, items[], diagnostics[] } }`
|
|
563
|
-
-
|
|
565
|
+
- `filters` can include `polymarketTagIds[]` when using `--polymarket-tag-id|--polymarket-tag-ids` (or `--sport-tag-id|--sport-tag-ids` aliases).
|
|
566
|
+
- each candidate row can include `eventId`, `eventSlug`, `eventTitle`, and `existingMirror: { marketAddress, similarity } | null`.
|
|
564
567
|
- `mirror go`:
|
|
565
568
|
- `{ ok: true, command: "mirror.go", data: { schemaVersion, generatedAt, mode, plan, deploy, verify, sync, polymarketPreflight, suggestedSyncCommand, trustManifest, diagnostics[] } }`
|
|
566
569
|
- `plan` is the same payload shape as `mirror.plan`; `deploy` is the same payload shape as `mirror.deploy`; `sync` is null unless `--auto-sync` is used.
|
|
@@ -91,7 +91,7 @@ function createRunMirrorCommand(deps) {
|
|
|
91
91
|
console.log('');
|
|
92
92
|
console.log('Subcommands:');
|
|
93
93
|
console.log(
|
|
94
|
-
' browse --min-yes-pct <n> --max-yes-pct <n> --min-volume-24h <n> [--closes-after <date>] [--closes-before <date>] [--question-contains <text>] [--limit <n>] [--chain-id <id>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>]',
|
|
94
|
+
' browse --min-yes-pct <n> --max-yes-pct <n> --min-volume-24h <n> [--closes-after <date>] [--closes-before <date>] [--question-contains <text>] [--limit <n>] [--chain-id <id>] [--polymarket-tag-id <id>] [--polymarket-tag-ids <csv>] [--sport-tag-id <id>] [--sport-tag-ids <csv>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>]',
|
|
95
95
|
);
|
|
96
96
|
console.log(
|
|
97
97
|
' plan --source polymarket --polymarket-market-id <id>|--polymarket-slug <slug> [--chain-id <id>] [--target-slippage-bps <n>] [--turnover-target <n>] [--depth-slippage-bps <n>] [--safety-multiplier <n>] [--min-liquidity-usdc <n>] [--max-liquidity-usdc <n>] [--with-rules] [--include-similarity] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>]',
|
|
@@ -22,12 +22,12 @@ module.exports = async function handleMirrorBrowse({ shared, context, deps }) {
|
|
|
22
22
|
context.outputMode,
|
|
23
23
|
'mirror.browse.help',
|
|
24
24
|
commandHelpPayload(
|
|
25
|
-
'pandora [--output table|json] mirror browse [--min-yes-pct <n>] [--max-yes-pct <n>] [--min-volume-24h <n>] [--closes-after <date>] [--closes-before <date>] [--question-contains <text>] [--limit <n>]',
|
|
25
|
+
'pandora [--output table|json] mirror browse [--min-yes-pct <n>] [--max-yes-pct <n>] [--min-volume-24h <n>] [--closes-after <date>] [--closes-before <date>] [--question-contains <text>] [--limit <n>] [--polymarket-tag-id <id>] [--polymarket-tag-ids <csv>] [--sport-tag-id <id>] [--sport-tag-ids <csv>]',
|
|
26
26
|
),
|
|
27
27
|
);
|
|
28
28
|
} else {
|
|
29
29
|
console.log(
|
|
30
|
-
'Usage: pandora [--output table|json] mirror browse [--min-yes-pct <n>] [--max-yes-pct <n>] [--min-volume-24h <n>] [--closes-after <date>] [--closes-before <date>] [--question-contains <text>] [--limit <n>]',
|
|
30
|
+
'Usage: pandora [--output table|json] mirror browse [--min-yes-pct <n>] [--max-yes-pct <n>] [--min-volume-24h <n>] [--closes-after <date>] [--closes-before <date>] [--question-contains <text>] [--limit <n>] [--polymarket-tag-id <id>] [--polymarket-tag-ids <csv>] [--sport-tag-id <id>] [--sport-tag-ids <csv>]',
|
|
31
31
|
);
|
|
32
32
|
}
|
|
33
33
|
return;
|
|
@@ -395,6 +395,7 @@ async function browseMirrorMarkets(options = {}) {
|
|
|
395
395
|
gammaUrl: options.polymarketGammaUrl,
|
|
396
396
|
gammaMockUrl: options.polymarketGammaMockUrl,
|
|
397
397
|
mockUrl: options.polymarketMockUrl,
|
|
398
|
+
polymarketTagIds: Array.isArray(options.polymarketTagIds) ? options.polymarketTagIds : [],
|
|
398
399
|
timeoutMs: options.timeoutMs,
|
|
399
400
|
minYesPct: options.minYesPct,
|
|
400
401
|
maxYesPct: options.maxYesPct,
|
|
@@ -39,8 +39,13 @@ function createParseMirrorBrowseFlags(deps) {
|
|
|
39
39
|
polymarketGammaUrl: null,
|
|
40
40
|
polymarketGammaMockUrl: null,
|
|
41
41
|
polymarketMockUrl: null,
|
|
42
|
+
polymarketTagIds: [],
|
|
42
43
|
};
|
|
43
44
|
|
|
45
|
+
function pushTagId(rawValue, flagName) {
|
|
46
|
+
options.polymarketTagIds.push(parsePositiveInteger(rawValue, flagName));
|
|
47
|
+
}
|
|
48
|
+
|
|
44
49
|
for (let i = 0; i < args.length; i += 1) {
|
|
45
50
|
const token = args[i];
|
|
46
51
|
if (token === '--min-yes-pct') {
|
|
@@ -98,6 +103,26 @@ function createParseMirrorBrowseFlags(deps) {
|
|
|
98
103
|
i += 1;
|
|
99
104
|
continue;
|
|
100
105
|
}
|
|
106
|
+
if (token === '--polymarket-tag-id' || token === '--sport-tag-id') {
|
|
107
|
+
pushTagId(requireFlagValue(args, i, token), token);
|
|
108
|
+
i += 1;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (token === '--polymarket-tag-ids' || token === '--sport-tag-ids') {
|
|
112
|
+
const raw = requireFlagValue(args, i, token);
|
|
113
|
+
const values = String(raw)
|
|
114
|
+
.split(',')
|
|
115
|
+
.map((value) => value.trim())
|
|
116
|
+
.filter(Boolean);
|
|
117
|
+
if (!values.length) {
|
|
118
|
+
throw new CliError('INVALID_FLAG_VALUE', `${token} must include at least one positive integer tag id.`);
|
|
119
|
+
}
|
|
120
|
+
for (const value of values) {
|
|
121
|
+
pushTagId(value, token);
|
|
122
|
+
}
|
|
123
|
+
i += 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
101
126
|
throw new CliError('UNKNOWN_FLAG', `Unknown flag for mirror browse: ${token}`);
|
|
102
127
|
}
|
|
103
128
|
|
|
@@ -105,6 +130,10 @@ function createParseMirrorBrowseFlags(deps) {
|
|
|
105
130
|
throw new CliError('INVALID_ARGS', '--min-yes-pct cannot be greater than --max-yes-pct.');
|
|
106
131
|
}
|
|
107
132
|
|
|
133
|
+
if (options.polymarketTagIds.length) {
|
|
134
|
+
options.polymarketTagIds = Array.from(new Set(options.polymarketTagIds));
|
|
135
|
+
}
|
|
136
|
+
|
|
108
137
|
return options;
|
|
109
138
|
};
|
|
110
139
|
}
|
|
@@ -405,6 +405,9 @@ function normalizeMarketRow(row) {
|
|
|
405
405
|
).trim() || null,
|
|
406
406
|
slug: String((row && (row.market_slug || row.marketSlug || row.slug)) || '').trim() || null,
|
|
407
407
|
question: extractQuestionText(row),
|
|
408
|
+
eventId: toStringOrNull(row && (row.event_id || row.eventId)),
|
|
409
|
+
eventSlug: toStringOrNull(row && (row.event_slug || row.eventSlug)),
|
|
410
|
+
eventTitle: toStringOrNull(row && (row.event_title || row.eventTitle)),
|
|
408
411
|
description: rulesSections.length ? rulesSections.join('\n\n') : null,
|
|
409
412
|
closeTimestamp: toTimestampSeconds(
|
|
410
413
|
row &&
|
|
@@ -479,6 +482,14 @@ function parseMarketsPayload(payload) {
|
|
|
479
482
|
return [];
|
|
480
483
|
}
|
|
481
484
|
|
|
485
|
+
function parseEventsPayload(payload) {
|
|
486
|
+
if (Array.isArray(payload)) return payload;
|
|
487
|
+
if (payload && Array.isArray(payload.events)) return payload.events;
|
|
488
|
+
if (payload && payload.data && Array.isArray(payload.data.events)) return payload.data.events;
|
|
489
|
+
if (payload && Array.isArray(payload.data)) return payload.data;
|
|
490
|
+
return [];
|
|
491
|
+
}
|
|
492
|
+
|
|
482
493
|
async function fetchJson(url, timeoutMs) {
|
|
483
494
|
const controller = new AbortController();
|
|
484
495
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -530,6 +541,15 @@ function buildGammaUrl(baseUrl, params) {
|
|
|
530
541
|
return url.toString();
|
|
531
542
|
}
|
|
532
543
|
|
|
544
|
+
function buildGammaEventsUrl(baseUrl, params) {
|
|
545
|
+
const url = new URL(`${baseUrl}/events`);
|
|
546
|
+
for (const [key, value] of Object.entries(params || {})) {
|
|
547
|
+
if (value === null || value === undefined || value === '') continue;
|
|
548
|
+
url.searchParams.set(key, String(value));
|
|
549
|
+
}
|
|
550
|
+
return url.toString();
|
|
551
|
+
}
|
|
552
|
+
|
|
533
553
|
async function fetchGammaRows(params, options = {}, diagnostics = []) {
|
|
534
554
|
const timeoutMs = Number.isInteger(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 12_000;
|
|
535
555
|
const gammaUrl = normalizeGammaBaseUrl(options.gammaUrl);
|
|
@@ -543,6 +563,120 @@ async function fetchGammaRows(params, options = {}, diagnostics = []) {
|
|
|
543
563
|
}
|
|
544
564
|
}
|
|
545
565
|
|
|
566
|
+
function makeMarketDedupeKey(row) {
|
|
567
|
+
return (
|
|
568
|
+
normalizeText(
|
|
569
|
+
row &&
|
|
570
|
+
(row.condition_id ||
|
|
571
|
+
row.conditionId ||
|
|
572
|
+
row.question_id ||
|
|
573
|
+
row.questionId ||
|
|
574
|
+
row.market_id ||
|
|
575
|
+
row.marketId ||
|
|
576
|
+
row.id ||
|
|
577
|
+
row.slug),
|
|
578
|
+
) || null
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function flattenEventMarkets(events) {
|
|
583
|
+
const output = [];
|
|
584
|
+
const seen = new Set();
|
|
585
|
+
|
|
586
|
+
for (const event of Array.isArray(events) ? events : []) {
|
|
587
|
+
const markets = Array.isArray(event && event.markets) ? event.markets : [];
|
|
588
|
+
for (const market of markets) {
|
|
589
|
+
if (!market || typeof market !== 'object') continue;
|
|
590
|
+
const dedupeKey = makeMarketDedupeKey(market);
|
|
591
|
+
if (dedupeKey && seen.has(dedupeKey)) continue;
|
|
592
|
+
if (dedupeKey) seen.add(dedupeKey);
|
|
593
|
+
output.push({
|
|
594
|
+
...market,
|
|
595
|
+
event_id: event && event.id !== undefined ? event.id : null,
|
|
596
|
+
event_slug: event && event.slug ? event.slug : null,
|
|
597
|
+
event_title: event && event.title ? event.title : null,
|
|
598
|
+
event_tags: Array.isArray(event && event.tags) ? event.tags : [],
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return output;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function normalizeTagIdList(input) {
|
|
607
|
+
const values = Array.isArray(input) ? input : [];
|
|
608
|
+
const normalized = [];
|
|
609
|
+
for (const value of values) {
|
|
610
|
+
const numeric = Number(value);
|
|
611
|
+
if (!Number.isFinite(numeric)) continue;
|
|
612
|
+
const asInt = Math.trunc(numeric);
|
|
613
|
+
if (asInt <= 0) continue;
|
|
614
|
+
normalized.push(asInt);
|
|
615
|
+
}
|
|
616
|
+
return Array.from(new Set(normalized));
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async function fetchGammaRowsByTagIds(params, options = {}, diagnostics = []) {
|
|
620
|
+
const timeoutMs = Number.isInteger(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 12_000;
|
|
621
|
+
const gammaUrl = normalizeGammaBaseUrl(options.gammaUrl);
|
|
622
|
+
const tagIds = normalizeTagIdList(params && params.tagIds);
|
|
623
|
+
if (!tagIds.length) return [];
|
|
624
|
+
|
|
625
|
+
if (options.gammaMockUrl) {
|
|
626
|
+
try {
|
|
627
|
+
const payload = await fetchJson(options.gammaMockUrl, timeoutMs);
|
|
628
|
+
const events = parseEventsPayload(payload);
|
|
629
|
+
return flattenEventMarkets(events);
|
|
630
|
+
} catch (err) {
|
|
631
|
+
diagnostics.push(`Gamma sports-events request failed (${options.gammaMockUrl}): ${formatNetworkError(err)}`);
|
|
632
|
+
return [];
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const allRows = [];
|
|
637
|
+
const perTagResults = await Promise.all(
|
|
638
|
+
tagIds.map(async (tagId) => {
|
|
639
|
+
const queryParams = {
|
|
640
|
+
...(params || {}),
|
|
641
|
+
tag_id: tagId,
|
|
642
|
+
};
|
|
643
|
+
delete queryParams.tagIds;
|
|
644
|
+
const targetUrl = buildGammaEventsUrl(gammaUrl, queryParams);
|
|
645
|
+
try {
|
|
646
|
+
const payload = await fetchJson(targetUrl, timeoutMs);
|
|
647
|
+
const events = parseEventsPayload(payload);
|
|
648
|
+
return {
|
|
649
|
+
rows: flattenEventMarkets(events),
|
|
650
|
+
error: null,
|
|
651
|
+
};
|
|
652
|
+
} catch (err) {
|
|
653
|
+
return {
|
|
654
|
+
rows: [],
|
|
655
|
+
error: `Gamma sports-events request failed (${targetUrl}): ${formatNetworkError(err)}`,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
}),
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
for (const result of perTagResults) {
|
|
662
|
+
if (result.error) {
|
|
663
|
+
diagnostics.push(result.error);
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
allRows.push(...result.rows);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const deduped = [];
|
|
670
|
+
const seen = new Set();
|
|
671
|
+
for (const row of allRows) {
|
|
672
|
+
const dedupeKey = makeMarketDedupeKey(row);
|
|
673
|
+
if (dedupeKey && seen.has(dedupeKey)) continue;
|
|
674
|
+
if (dedupeKey) seen.add(dedupeKey);
|
|
675
|
+
deduped.push(row);
|
|
676
|
+
}
|
|
677
|
+
return deduped;
|
|
678
|
+
}
|
|
679
|
+
|
|
546
680
|
function extractConditionId(row) {
|
|
547
681
|
const value = toStringOrNull(
|
|
548
682
|
row &&
|
|
@@ -998,21 +1132,38 @@ async function browsePolymarketMarkets(options = {}) {
|
|
|
998
1132
|
const timeoutMs = Number.isInteger(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 12_000;
|
|
999
1133
|
const requestedLimit = Number.isInteger(Number(options.limit)) && Number(options.limit) > 0 ? Number(options.limit) : 10;
|
|
1000
1134
|
const scanLimit = Math.max(requestedLimit * 5, 100);
|
|
1135
|
+
const polymarketTagIds = normalizeTagIdList(options.polymarketTagIds);
|
|
1136
|
+
const useSportsEventsEndpoint = polymarketTagIds.length > 0;
|
|
1001
1137
|
|
|
1002
1138
|
let rows = [];
|
|
1139
|
+
let sourceType = options.mockUrl ? 'polymarket:mock' : 'polymarket:gamma';
|
|
1003
1140
|
if (options.mockUrl) {
|
|
1004
1141
|
const payload = await fetchMockPayload(options.mockUrl, timeoutMs);
|
|
1005
1142
|
rows = parseMarketsPayload(payload);
|
|
1006
1143
|
} else {
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1144
|
+
if (useSportsEventsEndpoint) {
|
|
1145
|
+
rows = await fetchGammaRowsByTagIds(
|
|
1146
|
+
{
|
|
1147
|
+
tagIds: polymarketTagIds,
|
|
1148
|
+
active: true,
|
|
1149
|
+
closed: false,
|
|
1150
|
+
limit: Math.min(scanLimit, 500),
|
|
1151
|
+
},
|
|
1152
|
+
options,
|
|
1153
|
+
diagnostics,
|
|
1154
|
+
);
|
|
1155
|
+
sourceType = 'polymarket:gamma-events';
|
|
1156
|
+
} else {
|
|
1157
|
+
rows = await fetchGammaRows(
|
|
1158
|
+
{
|
|
1159
|
+
active: true,
|
|
1160
|
+
closed: false,
|
|
1161
|
+
limit: Math.min(scanLimit, 500),
|
|
1162
|
+
},
|
|
1163
|
+
options,
|
|
1164
|
+
diagnostics,
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1016
1167
|
}
|
|
1017
1168
|
|
|
1018
1169
|
const minYesPct = toOptionalNumber(options.minYesPct);
|
|
@@ -1039,6 +1190,9 @@ async function browsePolymarketMarkets(options = {}) {
|
|
|
1039
1190
|
const items = filtered.slice(0, requestedLimit).map((item) => ({
|
|
1040
1191
|
marketId: item.marketId,
|
|
1041
1192
|
slug: item.slug,
|
|
1193
|
+
eventId: item.eventId,
|
|
1194
|
+
eventSlug: item.eventSlug,
|
|
1195
|
+
eventTitle: item.eventTitle,
|
|
1042
1196
|
question: item.question,
|
|
1043
1197
|
closeTimestamp: item.closeTimestamp,
|
|
1044
1198
|
yesPct: item.yesPct,
|
|
@@ -1048,16 +1202,16 @@ async function browsePolymarketMarkets(options = {}) {
|
|
|
1048
1202
|
active: item.active,
|
|
1049
1203
|
resolved: item.resolved,
|
|
1050
1204
|
url: item.url,
|
|
1051
|
-
sourceType: item.source ||
|
|
1205
|
+
sourceType: item.source || sourceType,
|
|
1052
1206
|
}));
|
|
1053
1207
|
|
|
1054
1208
|
const gammaApiError =
|
|
1055
|
-
diagnostics.find((line) => /^Gamma request failed/i.test(String(line || ''))) || null;
|
|
1209
|
+
diagnostics.find((line) => /^Gamma( sports-events)? request failed/i.test(String(line || ''))) || null;
|
|
1056
1210
|
|
|
1057
1211
|
return {
|
|
1058
1212
|
schemaVersion: '1.0.0',
|
|
1059
1213
|
generatedAt: new Date().toISOString(),
|
|
1060
|
-
source:
|
|
1214
|
+
source: sourceType,
|
|
1061
1215
|
filters: {
|
|
1062
1216
|
minYesPct,
|
|
1063
1217
|
maxYesPct,
|
|
@@ -1066,6 +1220,7 @@ async function browsePolymarketMarkets(options = {}) {
|
|
|
1066
1220
|
closesBefore,
|
|
1067
1221
|
questionContains: options.questionContains || null,
|
|
1068
1222
|
limit: requestedLimit,
|
|
1223
|
+
polymarketTagIds,
|
|
1069
1224
|
},
|
|
1070
1225
|
count: items.length,
|
|
1071
1226
|
items,
|
|
@@ -264,6 +264,13 @@ function buildCommandDescriptors() {
|
|
|
264
264
|
emits: ['sports.resolve.plan', 'sports.help'],
|
|
265
265
|
dataSchema: '#/definitions/SportsResolvePlanPayload',
|
|
266
266
|
}),
|
|
267
|
+
'mirror.browse': commandDescriptor({
|
|
268
|
+
summary: 'Browse Polymarket mirror candidates with optional sports tag filters.',
|
|
269
|
+
usage:
|
|
270
|
+
'pandora [--output table|json] mirror browse [--min-yes-pct <n>] [--max-yes-pct <n>] [--min-volume-24h <n>] [--closes-after <date>] [--closes-before <date>] [--question-contains <text>] [--limit <n>] [--chain-id <id>] [--polymarket-tag-id <id>] [--polymarket-tag-ids <csv>] [--sport-tag-id <id>] [--sport-tag-ids <csv>] [--polymarket-gamma-url <url>] [--polymarket-gamma-mock-url <url>] [--polymarket-mock-url <url>]',
|
|
271
|
+
emits: ['mirror.browse', 'mirror.browse.help'],
|
|
272
|
+
dataSchema: '#/definitions/MirrorBrowsePayload',
|
|
273
|
+
}),
|
|
267
274
|
'mirror.plan': commandDescriptor({
|
|
268
275
|
summary: 'Generate mirror sizing/distribution plan from Polymarket source.',
|
|
269
276
|
usage:
|
|
@@ -568,6 +575,9 @@ function buildSchemaPayload() {
|
|
|
568
575
|
mode: { enum: ['dry-run', 'execute'] },
|
|
569
576
|
status: { type: 'string' },
|
|
570
577
|
marketAddress: { type: 'string' },
|
|
578
|
+
marketType: { type: ['string', 'null'] },
|
|
579
|
+
buySignature: { type: ['string', 'null'] },
|
|
580
|
+
ammDeadlineEpoch: { type: ['string', 'null'] },
|
|
571
581
|
side: { enum: ['yes', 'no'] },
|
|
572
582
|
amountUsdc: { type: 'number' },
|
|
573
583
|
quote: { type: 'object' },
|
|
@@ -809,6 +819,19 @@ function buildSchemaPayload() {
|
|
|
809
819
|
generatedAt: { type: 'string', format: 'date-time' },
|
|
810
820
|
},
|
|
811
821
|
},
|
|
822
|
+
MirrorBrowsePayload: {
|
|
823
|
+
type: 'object',
|
|
824
|
+
properties: {
|
|
825
|
+
source: { type: 'string' },
|
|
826
|
+
gammaApiError: { type: ['string', 'null'] },
|
|
827
|
+
filters: { type: 'object' },
|
|
828
|
+
count: { type: 'integer' },
|
|
829
|
+
items: { type: 'array', items: { type: 'object' } },
|
|
830
|
+
diagnostics: { type: 'array', items: { type: 'string' } },
|
|
831
|
+
schemaVersion: { type: 'string' },
|
|
832
|
+
generatedAt: { type: 'string', format: 'date-time' },
|
|
833
|
+
},
|
|
834
|
+
},
|
|
812
835
|
MirrorPlanPayload: {
|
|
813
836
|
type: 'object',
|
|
814
837
|
properties: {
|
|
@@ -117,6 +117,9 @@ function createRunTradeCommand(deps) {
|
|
|
117
117
|
},
|
|
118
118
|
chainId: execution.chainId,
|
|
119
119
|
marketAddress: options.marketAddress,
|
|
120
|
+
marketType: execution.marketType || null,
|
|
121
|
+
buySignature: execution.buySignature || null,
|
|
122
|
+
ammDeadlineEpoch: execution.ammDeadlineEpoch || null,
|
|
120
123
|
side: options.side,
|
|
121
124
|
amountUsdc: options.amountUsdc,
|
|
122
125
|
amountRaw: execution.amountRaw,
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
const PARI_MUTUEL_BUY_ABI = [
|
|
2
|
+
{
|
|
3
|
+
name: 'buy',
|
|
4
|
+
type: 'function',
|
|
5
|
+
stateMutability: 'nonpayable',
|
|
6
|
+
inputs: [
|
|
7
|
+
{ name: 'isYes', type: 'bool' },
|
|
8
|
+
{ name: 'collateralAmount', type: 'uint256' },
|
|
9
|
+
{ name: 'minSharesOut', type: 'uint256' },
|
|
10
|
+
],
|
|
11
|
+
outputs: [{ name: 'sharesOut', type: 'uint256' }],
|
|
12
|
+
},
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const PREDICTION_AMM_BUY_ABI = [
|
|
16
|
+
{
|
|
17
|
+
name: 'buy',
|
|
18
|
+
type: 'function',
|
|
19
|
+
stateMutability: 'nonpayable',
|
|
20
|
+
inputs: [
|
|
21
|
+
{ name: 'isYes', type: 'bool' },
|
|
22
|
+
{ name: 'collateralAmount', type: 'uint256' },
|
|
23
|
+
{ name: 'minSharesOut', type: 'uint256' },
|
|
24
|
+
{ name: 'deadline', type: 'uint256' },
|
|
25
|
+
],
|
|
26
|
+
outputs: [{ name: 'sharesOut', type: 'uint256' }],
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const PARI_MUTUEL_MARKER_ABI = [
|
|
31
|
+
{
|
|
32
|
+
type: 'function',
|
|
33
|
+
name: 'curveFlattener',
|
|
34
|
+
stateMutability: 'view',
|
|
35
|
+
inputs: [],
|
|
36
|
+
outputs: [{ type: 'uint8' }],
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const PREDICTION_AMM_MARKER_ABI = [
|
|
41
|
+
{
|
|
42
|
+
type: 'function',
|
|
43
|
+
name: 'tradingFee',
|
|
44
|
+
stateMutability: 'view',
|
|
45
|
+
inputs: [],
|
|
46
|
+
outputs: [{ type: 'uint24' }],
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const DEFAULT_AMM_TRADE_DEADLINE_OFFSET_SEC = 15 * 60;
|
|
51
|
+
|
|
52
|
+
function createTradeTypeError(code, message, details = undefined) {
|
|
53
|
+
const err = new Error(message);
|
|
54
|
+
err.code = code;
|
|
55
|
+
if (details !== undefined) {
|
|
56
|
+
err.details = details;
|
|
57
|
+
}
|
|
58
|
+
return err;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Detects supported Pandora market type by probing stable view-method markers.
|
|
63
|
+
* @param {{readContract: Function}} publicClient
|
|
64
|
+
* @param {`0x${string}`} marketAddress
|
|
65
|
+
* @returns {Promise<{marketType:'parimutuel'|'amm', detectedBy:string}>}
|
|
66
|
+
*/
|
|
67
|
+
async function detectTradeMarketType(publicClient, marketAddress) {
|
|
68
|
+
let pariError = null;
|
|
69
|
+
try {
|
|
70
|
+
await publicClient.readContract({
|
|
71
|
+
address: marketAddress,
|
|
72
|
+
abi: PARI_MUTUEL_MARKER_ABI,
|
|
73
|
+
functionName: 'curveFlattener',
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
marketType: 'parimutuel',
|
|
77
|
+
detectedBy: 'curveFlattener',
|
|
78
|
+
};
|
|
79
|
+
} catch (err) {
|
|
80
|
+
pariError = err;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let ammError = null;
|
|
84
|
+
try {
|
|
85
|
+
await publicClient.readContract({
|
|
86
|
+
address: marketAddress,
|
|
87
|
+
abi: PREDICTION_AMM_MARKER_ABI,
|
|
88
|
+
functionName: 'tradingFee',
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
marketType: 'amm',
|
|
92
|
+
detectedBy: 'tradingFee',
|
|
93
|
+
};
|
|
94
|
+
} catch (err) {
|
|
95
|
+
ammError = err;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
throw createTradeTypeError(
|
|
99
|
+
'UNSUPPORTED_MARKET_TRADE_INTERFACE',
|
|
100
|
+
'Market does not expose a supported Pandora trade interface.',
|
|
101
|
+
{
|
|
102
|
+
marketAddress,
|
|
103
|
+
attemptedMarkers: ['curveFlattener', 'tradingFee'],
|
|
104
|
+
markerErrors: {
|
|
105
|
+
parimutuel: pariError && pariError.message ? pariError.message : String(pariError),
|
|
106
|
+
amm: ammError && ammError.message ? ammError.message : String(ammError),
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function toEpochSeconds(value, fallback) {
|
|
113
|
+
const numeric = Number(value);
|
|
114
|
+
if (Number.isFinite(numeric)) {
|
|
115
|
+
const truncated = Math.trunc(numeric);
|
|
116
|
+
if (truncated >= 0) return truncated;
|
|
117
|
+
}
|
|
118
|
+
return fallback;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Builds the market-specific buy call shape.
|
|
123
|
+
* @param {{
|
|
124
|
+
* marketType: 'parimutuel'|'amm',
|
|
125
|
+
* side: 'yes'|'no',
|
|
126
|
+
* amountRaw: bigint,
|
|
127
|
+
* minSharesOutRaw: bigint,
|
|
128
|
+
* nowEpochSec?: number,
|
|
129
|
+
* ammDeadlineOffsetSec?: number,
|
|
130
|
+
* }} input
|
|
131
|
+
* @returns {{marketType:'parimutuel'|'amm', abi: object[], functionName: 'buy', args: (boolean|bigint)[], signature: string, ammDeadlineEpoch?: string}}
|
|
132
|
+
*/
|
|
133
|
+
function buildTradeBuyCall(input) {
|
|
134
|
+
const marketType = String(input.marketType || '').toLowerCase();
|
|
135
|
+
const isYes = String(input.side || '').toLowerCase() === 'yes';
|
|
136
|
+
const amountRaw = input.amountRaw;
|
|
137
|
+
const minSharesOutRaw = input.minSharesOutRaw;
|
|
138
|
+
|
|
139
|
+
if (marketType === 'parimutuel') {
|
|
140
|
+
return {
|
|
141
|
+
marketType: 'parimutuel',
|
|
142
|
+
abi: PARI_MUTUEL_BUY_ABI,
|
|
143
|
+
functionName: 'buy',
|
|
144
|
+
args: [isYes, amountRaw, minSharesOutRaw],
|
|
145
|
+
signature: 'buy(bool,uint256,uint256)',
|
|
146
|
+
ammDeadlineEpoch: null,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (marketType === 'amm') {
|
|
151
|
+
const nowEpochSec = toEpochSeconds(input.nowEpochSec, Math.trunc(Date.now() / 1000));
|
|
152
|
+
const offsetSec = toEpochSeconds(input.ammDeadlineOffsetSec, DEFAULT_AMM_TRADE_DEADLINE_OFFSET_SEC);
|
|
153
|
+
const deadlineEpoch = BigInt(nowEpochSec + Math.max(1, offsetSec));
|
|
154
|
+
return {
|
|
155
|
+
marketType: 'amm',
|
|
156
|
+
abi: PREDICTION_AMM_BUY_ABI,
|
|
157
|
+
functionName: 'buy',
|
|
158
|
+
args: [isYes, amountRaw, minSharesOutRaw, deadlineEpoch],
|
|
159
|
+
signature: 'buy(bool,uint256,uint256,uint256)',
|
|
160
|
+
ammDeadlineEpoch: deadlineEpoch.toString(),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
throw createTradeTypeError('UNSUPPORTED_MARKET_TYPE', `Unsupported market type for trade execution: ${marketType}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Resolves market type then constructs the correct buy call descriptor.
|
|
169
|
+
* @param {{
|
|
170
|
+
* publicClient: { readContract: Function },
|
|
171
|
+
* marketAddress: `0x${string}`,
|
|
172
|
+
* side: 'yes'|'no',
|
|
173
|
+
* amountRaw: bigint,
|
|
174
|
+
* minSharesOutRaw: bigint,
|
|
175
|
+
* nowEpochSec?: number,
|
|
176
|
+
* ammDeadlineOffsetSec?: number,
|
|
177
|
+
* }} input
|
|
178
|
+
* @returns {Promise<{marketType:'parimutuel'|'amm', detectedBy:string, abi: object[], functionName: 'buy', args: (boolean|bigint)[], signature: string, ammDeadlineEpoch?: string}>}
|
|
179
|
+
*/
|
|
180
|
+
async function resolveTradeBuyCall(input) {
|
|
181
|
+
const detected = await detectTradeMarketType(input.publicClient, input.marketAddress);
|
|
182
|
+
const call = buildTradeBuyCall({
|
|
183
|
+
marketType: detected.marketType,
|
|
184
|
+
side: input.side,
|
|
185
|
+
amountRaw: input.amountRaw,
|
|
186
|
+
minSharesOutRaw: input.minSharesOutRaw,
|
|
187
|
+
nowEpochSec: input.nowEpochSec,
|
|
188
|
+
ammDeadlineOffsetSec: input.ammDeadlineOffsetSec,
|
|
189
|
+
});
|
|
190
|
+
return {
|
|
191
|
+
...call,
|
|
192
|
+
detectedBy: detected.detectedBy,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
PARI_MUTUEL_BUY_ABI,
|
|
198
|
+
PREDICTION_AMM_BUY_ABI,
|
|
199
|
+
DEFAULT_AMM_TRADE_DEADLINE_OFFSET_SEC,
|
|
200
|
+
detectTradeMarketType,
|
|
201
|
+
buildTradeBuyCall,
|
|
202
|
+
resolveTradeBuyCall,
|
|
203
|
+
};
|
package/cli/pandora.cjs
CHANGED
|
@@ -60,6 +60,7 @@ const { createRunOddsCommand } = require('./lib/odds_command_service.cjs');
|
|
|
60
60
|
const { createRunSportsCommand } = require('./lib/sports_command_service.cjs');
|
|
61
61
|
const { createRunRiskCommand } = require('./lib/risk_command_service.cjs');
|
|
62
62
|
const { createRunModelCommand } = require('./lib/model_command_service.cjs');
|
|
63
|
+
const { resolveTradeBuyCall } = require('./lib/trade_market_type_service.cjs');
|
|
63
64
|
const {
|
|
64
65
|
DEFAULT_INDEXER_URL: SHARED_DEFAULT_INDEXER_URL,
|
|
65
66
|
DEFAULT_RPC_BY_CHAIN_ID,
|
|
@@ -598,20 +599,6 @@ const ERC20_ABI = [
|
|
|
598
599
|
outputs: [{ type: 'uint256' }],
|
|
599
600
|
},
|
|
600
601
|
];
|
|
601
|
-
const PARI_MUTUEL_ABI = [
|
|
602
|
-
{
|
|
603
|
-
name: 'buy',
|
|
604
|
-
type: 'function',
|
|
605
|
-
stateMutability: 'nonpayable',
|
|
606
|
-
inputs: [
|
|
607
|
-
{ name: 'isYes', type: 'bool' },
|
|
608
|
-
{ name: 'collateralAmount', type: 'uint256' },
|
|
609
|
-
{ name: 'minSharesOut', type: 'uint256' },
|
|
610
|
-
],
|
|
611
|
-
outputs: [{ name: 'sharesOut', type: 'uint256' }],
|
|
612
|
-
},
|
|
613
|
-
];
|
|
614
|
-
|
|
615
602
|
const MARKET_DIRECT_ODDS_FIELDS = [
|
|
616
603
|
{ yesField: 'yesPct', noField: 'noPct', source: 'direct:yesPct/noPct' },
|
|
617
604
|
{ yesField: 'yesOdds', noField: 'noOdds', source: 'direct:yesOdds/noOdds' },
|
|
@@ -897,10 +884,10 @@ Usage:
|
|
|
897
884
|
|
|
898
885
|
Notes:
|
|
899
886
|
- --dry-run prints the execution plan and quote without sending transactions.
|
|
900
|
-
- --execute performs allowance check, optional USDC approve, then calls buy(
|
|
887
|
+
- --execute performs allowance check, optional USDC approve, then calls market buy() using the detected market ABI.
|
|
901
888
|
- --max-amount-usdc and probability guard flags fail fast before execution.
|
|
902
889
|
- --execute requires a quote by default unless --min-shares-out-raw or --allow-unquoted-execute is set.
|
|
903
|
-
-
|
|
890
|
+
- Supports both PariMutuel and AMM market buy signatures.
|
|
904
891
|
`);
|
|
905
892
|
}
|
|
906
893
|
|
|
@@ -3604,6 +3591,28 @@ async function executeTradeOnchain(options) {
|
|
|
3604
3591
|
});
|
|
3605
3592
|
};
|
|
3606
3593
|
|
|
3594
|
+
let buyCall;
|
|
3595
|
+
try {
|
|
3596
|
+
buyCall = await resolveTradeBuyCall({
|
|
3597
|
+
publicClient,
|
|
3598
|
+
marketAddress: options.marketAddress,
|
|
3599
|
+
side: options.side,
|
|
3600
|
+
amountRaw,
|
|
3601
|
+
minSharesOutRaw,
|
|
3602
|
+
});
|
|
3603
|
+
} catch (error) {
|
|
3604
|
+
if (error && error.code) {
|
|
3605
|
+
throw new CliError(
|
|
3606
|
+
error.code,
|
|
3607
|
+
error.message || 'Unsupported market trade interface.',
|
|
3608
|
+
error.details,
|
|
3609
|
+
);
|
|
3610
|
+
}
|
|
3611
|
+
await decodeTradeError(error, 'TRADE_MARKET_TYPE_RESOLUTION_FAILED', 'Unable to resolve market trade interface.', {
|
|
3612
|
+
stage: 'market-type-resolve',
|
|
3613
|
+
});
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3607
3616
|
let allowance;
|
|
3608
3617
|
try {
|
|
3609
3618
|
allowance = await publicClient.readContract({
|
|
@@ -3660,9 +3669,9 @@ async function executeTradeOnchain(options) {
|
|
|
3660
3669
|
const buySimulation = await publicClient.simulateContract({
|
|
3661
3670
|
account,
|
|
3662
3671
|
address: options.marketAddress,
|
|
3663
|
-
abi:
|
|
3664
|
-
functionName:
|
|
3665
|
-
args:
|
|
3672
|
+
abi: buyCall.abi,
|
|
3673
|
+
functionName: buyCall.functionName,
|
|
3674
|
+
args: buyCall.args,
|
|
3666
3675
|
});
|
|
3667
3676
|
buyGasEstimate =
|
|
3668
3677
|
buySimulation && buySimulation.request && buySimulation.request.gas
|
|
@@ -3675,6 +3684,9 @@ async function executeTradeOnchain(options) {
|
|
|
3675
3684
|
await decodeTradeError(error, 'TRADE_EXECUTION_FAILED', 'Buy transaction failed.', {
|
|
3676
3685
|
stage: 'buy',
|
|
3677
3686
|
buyTxHash,
|
|
3687
|
+
marketType: buyCall ? buyCall.marketType : null,
|
|
3688
|
+
buySignature: buyCall ? buyCall.signature : null,
|
|
3689
|
+
ammDeadlineEpoch: buyCall && buyCall.ammDeadlineEpoch ? buyCall.ammDeadlineEpoch : null,
|
|
3678
3690
|
});
|
|
3679
3691
|
}
|
|
3680
3692
|
|
|
@@ -3684,6 +3696,9 @@ async function executeTradeOnchain(options) {
|
|
|
3684
3696
|
rpcUrl: runtime.rpcUrl,
|
|
3685
3697
|
account: account.address,
|
|
3686
3698
|
usdc: runtime.usdcAddress,
|
|
3699
|
+
marketType: buyCall.marketType,
|
|
3700
|
+
buySignature: buyCall.signature,
|
|
3701
|
+
ammDeadlineEpoch: buyCall.ammDeadlineEpoch,
|
|
3687
3702
|
amountRaw: amountRaw.toString(),
|
|
3688
3703
|
minSharesOutRaw: minSharesOutRaw.toString(),
|
|
3689
3704
|
approveTxHash,
|
package/package.json
CHANGED
|
@@ -1090,6 +1090,9 @@ test('schema command returns envelope schema plus command descriptors', () => {
|
|
|
1090
1090
|
assert.ok(payload.data.commandDescriptors.quote.emits.includes('quote'));
|
|
1091
1091
|
assert.ok(payload.data.commandDescriptors.trade);
|
|
1092
1092
|
assert.equal(payload.data.commandDescriptors.trade.dataSchema, '#/definitions/TradePayload');
|
|
1093
|
+
assert.ok(payload.data.commandDescriptors['mirror.browse']);
|
|
1094
|
+
assert.equal(payload.data.commandDescriptors['mirror.browse'].dataSchema, '#/definitions/MirrorBrowsePayload');
|
|
1095
|
+
assert.match(payload.data.commandDescriptors['mirror.browse'].usage, /--polymarket-tag-id/);
|
|
1093
1096
|
assert.ok(payload.data.commandDescriptors['mirror.plan']);
|
|
1094
1097
|
assert.equal(payload.data.commandDescriptors['mirror.plan'].dataSchema, '#/definitions/MirrorPlanPayload');
|
|
1095
1098
|
assert.ok(payload.data.commandDescriptors['risk.show']);
|
|
@@ -1150,6 +1153,7 @@ test('schema command returns envelope schema plus command descriptors', () => {
|
|
|
1150
1153
|
assert.ok(payload.data.definitions.ModelCorrelationPayload);
|
|
1151
1154
|
assert.ok(payload.data.definitions.ModelDiagnosePayload);
|
|
1152
1155
|
assert.ok(payload.data.definitions.ErrorRecoveryPayload);
|
|
1156
|
+
assert.ok(payload.data.definitions.MirrorBrowsePayload);
|
|
1153
1157
|
});
|
|
1154
1158
|
|
|
1155
1159
|
test('schema command rejects unknown trailing flags', () => {
|
|
@@ -4825,6 +4829,24 @@ test('mirror browse rejects invalid calendar rollover dates', () => {
|
|
|
4825
4829
|
assert.match(payload.error.message, /real calendar date/);
|
|
4826
4830
|
});
|
|
4827
4831
|
|
|
4832
|
+
test('mirror browse rejects invalid tag id values', () => {
|
|
4833
|
+
const result = runCli(['--output', 'json', 'mirror', 'browse', '--polymarket-tag-id', '0']);
|
|
4834
|
+
assert.equal(result.status, 1);
|
|
4835
|
+
const payload = parseJsonOutput(result);
|
|
4836
|
+
assert.equal(payload.ok, false);
|
|
4837
|
+
assert.equal(payload.error.code, 'INVALID_FLAG_VALUE');
|
|
4838
|
+
assert.match(payload.error.message, /--polymarket-tag-id must be a positive integer/i);
|
|
4839
|
+
});
|
|
4840
|
+
|
|
4841
|
+
test('mirror browse rejects empty tag-id csv values', () => {
|
|
4842
|
+
const result = runCli(['--output', 'json', 'mirror', 'browse', '--polymarket-tag-ids', ', ,']);
|
|
4843
|
+
assert.equal(result.status, 1);
|
|
4844
|
+
const payload = parseJsonOutput(result);
|
|
4845
|
+
assert.equal(payload.ok, false);
|
|
4846
|
+
assert.equal(payload.error.code, 'INVALID_FLAG_VALUE');
|
|
4847
|
+
assert.match(payload.error.message, /must include at least one positive integer tag id/i);
|
|
4848
|
+
});
|
|
4849
|
+
|
|
4828
4850
|
test('boolean flags with --key=false do not silently flip behavior', () => {
|
|
4829
4851
|
const result = runCli(['--output', 'json', 'scan', '--active=false']);
|
|
4830
4852
|
assert.equal(result.status, 1);
|
|
@@ -4924,6 +4946,83 @@ test('mirror browse returns candidate markets with existing mirror hint', async
|
|
|
4924
4946
|
}
|
|
4925
4947
|
});
|
|
4926
4948
|
|
|
4949
|
+
test('mirror browse supports sports tag filters via gamma events endpoint', async () => {
|
|
4950
|
+
const gamma = await startJsonHttpServer((request) => {
|
|
4951
|
+
const parsed = new URL(request.url || '/', 'http://127.0.0.1');
|
|
4952
|
+
if (parsed.pathname !== '/events') {
|
|
4953
|
+
return { status: 404, body: { error: 'not found' } };
|
|
4954
|
+
}
|
|
4955
|
+
|
|
4956
|
+
const tagId = parsed.searchParams.get('tag_id');
|
|
4957
|
+
if (tagId !== '82') {
|
|
4958
|
+
return { body: { events: [] } };
|
|
4959
|
+
}
|
|
4960
|
+
|
|
4961
|
+
return {
|
|
4962
|
+
body: {
|
|
4963
|
+
events: [
|
|
4964
|
+
{
|
|
4965
|
+
id: 'evt-epl-1',
|
|
4966
|
+
slug: 'everton-v-burnley',
|
|
4967
|
+
title: 'Everton vs Burnley',
|
|
4968
|
+
markets: [
|
|
4969
|
+
{
|
|
4970
|
+
condition_id: 'poly-epl-c1',
|
|
4971
|
+
market_slug: 'everton-v-burnley-home',
|
|
4972
|
+
question: 'Will Everton beat Burnley?',
|
|
4973
|
+
end_date_iso: FIXED_MIRROR_CLOSE_ISO,
|
|
4974
|
+
active: true,
|
|
4975
|
+
closed: false,
|
|
4976
|
+
volume24hr: 550000,
|
|
4977
|
+
tokens: [
|
|
4978
|
+
{ outcome: 'Yes', price: '0.605', token_id: 'poly-epl-yes-1' },
|
|
4979
|
+
{ outcome: 'No', price: '0.395', token_id: 'poly-epl-no-1' },
|
|
4980
|
+
],
|
|
4981
|
+
},
|
|
4982
|
+
],
|
|
4983
|
+
},
|
|
4984
|
+
],
|
|
4985
|
+
},
|
|
4986
|
+
};
|
|
4987
|
+
});
|
|
4988
|
+
|
|
4989
|
+
try {
|
|
4990
|
+
const result = await runCliAsync([
|
|
4991
|
+
'--output',
|
|
4992
|
+
'json',
|
|
4993
|
+
'mirror',
|
|
4994
|
+
'browse',
|
|
4995
|
+
'--skip-dotenv',
|
|
4996
|
+
'--polymarket-gamma-url',
|
|
4997
|
+
gamma.url,
|
|
4998
|
+
'--polymarket-tag-id',
|
|
4999
|
+
'82',
|
|
5000
|
+
'--limit',
|
|
5001
|
+
'5',
|
|
5002
|
+
]);
|
|
5003
|
+
|
|
5004
|
+
assert.equal(result.status, 0);
|
|
5005
|
+
const payload = parseJsonOutput(result);
|
|
5006
|
+
assert.equal(payload.ok, true);
|
|
5007
|
+
assert.equal(payload.command, 'mirror.browse');
|
|
5008
|
+
assert.equal(payload.data.source, 'polymarket:gamma-events');
|
|
5009
|
+
assert.deepEqual(payload.data.filters.polymarketTagIds, [82]);
|
|
5010
|
+
assert.equal(payload.data.count, 1);
|
|
5011
|
+
assert.equal(payload.data.items[0].eventSlug, 'everton-v-burnley');
|
|
5012
|
+
assert.equal(payload.data.items[0].eventTitle, 'Everton vs Burnley');
|
|
5013
|
+
assert.equal(payload.data.items[0].eventId, 'evt-epl-1');
|
|
5014
|
+
|
|
5015
|
+
const eventRequest = gamma.requests.find((entry) => String(entry.url || '').startsWith('/events?'));
|
|
5016
|
+
assert.ok(eventRequest);
|
|
5017
|
+
const parsed = new URL(eventRequest.url, 'http://127.0.0.1');
|
|
5018
|
+
assert.equal(parsed.searchParams.get('tag_id'), '82');
|
|
5019
|
+
assert.equal(parsed.searchParams.get('active'), 'true');
|
|
5020
|
+
assert.equal(parsed.searchParams.get('closed'), 'false');
|
|
5021
|
+
} finally {
|
|
5022
|
+
await gamma.close();
|
|
5023
|
+
}
|
|
5024
|
+
});
|
|
5025
|
+
|
|
4927
5026
|
test('mirror sync accepts --market-address with --dry-run mode alias', async () => {
|
|
4928
5027
|
const tempDir = createTempDir('pandora-mirror-sync-aliases-');
|
|
4929
5028
|
const stateFile = path.join(tempDir, 'mirror-state.json');
|
|
@@ -1027,6 +1027,124 @@ test('browsePolymarketMarkets filters mock payload deterministically', async ()
|
|
|
1027
1027
|
}
|
|
1028
1028
|
});
|
|
1029
1029
|
|
|
1030
|
+
test('browsePolymarketMarkets uses gamma events endpoint for tag-id sports discovery', async () => {
|
|
1031
|
+
const requests = [];
|
|
1032
|
+
const server = http.createServer((req, res) => {
|
|
1033
|
+
requests.push(req.url || '/');
|
|
1034
|
+
const parsed = new URL(req.url || '/', 'http://127.0.0.1');
|
|
1035
|
+
const tagId = parsed.searchParams.get('tag_id');
|
|
1036
|
+
|
|
1037
|
+
res.statusCode = 200;
|
|
1038
|
+
res.setHeader('content-type', 'application/json');
|
|
1039
|
+
|
|
1040
|
+
if (parsed.pathname !== '/events') {
|
|
1041
|
+
res.end(JSON.stringify({ events: [] }));
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (tagId === '82') {
|
|
1046
|
+
res.end(
|
|
1047
|
+
JSON.stringify({
|
|
1048
|
+
events: [
|
|
1049
|
+
{
|
|
1050
|
+
id: 'evt-82',
|
|
1051
|
+
slug: 'everton-v-burnley',
|
|
1052
|
+
title: 'Everton vs Burnley',
|
|
1053
|
+
markets: [
|
|
1054
|
+
{
|
|
1055
|
+
condition_id: 'sports-c1',
|
|
1056
|
+
market_slug: 'everton-v-burnley-home',
|
|
1057
|
+
question: 'Will Everton beat Burnley?',
|
|
1058
|
+
end_date_iso: '2030-03-09T16:00:00Z',
|
|
1059
|
+
active: true,
|
|
1060
|
+
closed: false,
|
|
1061
|
+
volume24hr: 500000,
|
|
1062
|
+
tokens: [
|
|
1063
|
+
{ outcome: 'Yes', price: '0.605', token_id: 'yes-sports-1' },
|
|
1064
|
+
{ outcome: 'No', price: '0.395', token_id: 'no-sports-1' },
|
|
1065
|
+
],
|
|
1066
|
+
},
|
|
1067
|
+
],
|
|
1068
|
+
},
|
|
1069
|
+
],
|
|
1070
|
+
}),
|
|
1071
|
+
);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
if (tagId === '100350') {
|
|
1076
|
+
res.end(
|
|
1077
|
+
JSON.stringify({
|
|
1078
|
+
events: [
|
|
1079
|
+
{
|
|
1080
|
+
id: 'evt-100350',
|
|
1081
|
+
slug: 'leeds-v-sunderland',
|
|
1082
|
+
title: 'Leeds vs Sunderland',
|
|
1083
|
+
markets: [
|
|
1084
|
+
{
|
|
1085
|
+
// Duplicate condition id should be deduped across tag-id scans.
|
|
1086
|
+
condition_id: 'sports-c1',
|
|
1087
|
+
market_slug: 'duplicate-market-ignored',
|
|
1088
|
+
question: 'Duplicate row should be ignored',
|
|
1089
|
+
end_date_iso: '2030-03-09T16:00:00Z',
|
|
1090
|
+
active: true,
|
|
1091
|
+
closed: false,
|
|
1092
|
+
volume24hr: 1,
|
|
1093
|
+
tokens: [
|
|
1094
|
+
{ outcome: 'Yes', price: '0.5', token_id: 'dup-yes' },
|
|
1095
|
+
{ outcome: 'No', price: '0.5', token_id: 'dup-no' },
|
|
1096
|
+
],
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
condition_id: 'sports-c2',
|
|
1100
|
+
market_slug: 'leeds-v-sunderland-home',
|
|
1101
|
+
question: 'Will Leeds beat Sunderland?',
|
|
1102
|
+
end_date_iso: '2030-03-09T16:00:00Z',
|
|
1103
|
+
active: true,
|
|
1104
|
+
closed: false,
|
|
1105
|
+
volume24hr: 400000,
|
|
1106
|
+
tokens: [
|
|
1107
|
+
{ outcome: 'Yes', price: '0.495', token_id: 'yes-sports-2' },
|
|
1108
|
+
{ outcome: 'No', price: '0.505', token_id: 'no-sports-2' },
|
|
1109
|
+
],
|
|
1110
|
+
},
|
|
1111
|
+
],
|
|
1112
|
+
},
|
|
1113
|
+
],
|
|
1114
|
+
}),
|
|
1115
|
+
);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
res.end(JSON.stringify({ events: [] }));
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
1123
|
+
const { port } = server.address();
|
|
1124
|
+
const gammaUrl = `http://127.0.0.1:${port}`;
|
|
1125
|
+
|
|
1126
|
+
try {
|
|
1127
|
+
const payload = await browsePolymarketMarkets({
|
|
1128
|
+
gammaUrl,
|
|
1129
|
+
polymarketTagIds: [82, 100350],
|
|
1130
|
+
limit: 10,
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
assert.equal(payload.source, 'polymarket:gamma-events');
|
|
1134
|
+
assert.deepEqual(payload.filters.polymarketTagIds, [82, 100350]);
|
|
1135
|
+
assert.equal(payload.count, 2);
|
|
1136
|
+
assert.equal(payload.items[0].eventSlug, 'everton-v-burnley');
|
|
1137
|
+
assert.equal(payload.items[1].eventSlug, 'leeds-v-sunderland');
|
|
1138
|
+
assert.equal(payload.items[0].eventTitle, 'Everton vs Burnley');
|
|
1139
|
+
assert.equal(payload.items[0].eventId, 'evt-82');
|
|
1140
|
+
|
|
1141
|
+
const eventRequests = requests.filter((entry) => String(entry).startsWith('/events?'));
|
|
1142
|
+
assert.equal(eventRequests.length, 2);
|
|
1143
|
+
} finally {
|
|
1144
|
+
await new Promise((resolve) => server.close(resolve));
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1030
1148
|
test('computeApprovalDiff deterministically marks missing allowance/operator checks', () => {
|
|
1031
1149
|
const payload = computeApprovalDiff({
|
|
1032
1150
|
ownerAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
DEFAULT_AMM_TRADE_DEADLINE_OFFSET_SEC,
|
|
6
|
+
detectTradeMarketType,
|
|
7
|
+
buildTradeBuyCall,
|
|
8
|
+
resolveTradeBuyCall,
|
|
9
|
+
} = require('../../cli/lib/trade_market_type_service.cjs');
|
|
10
|
+
|
|
11
|
+
const MARKET = '0x1111111111111111111111111111111111111111';
|
|
12
|
+
|
|
13
|
+
test('detectTradeMarketType resolves parimutuel marker first', async () => {
|
|
14
|
+
const publicClient = {
|
|
15
|
+
async readContract(request) {
|
|
16
|
+
if (request.functionName === 'curveFlattener') return 7n;
|
|
17
|
+
throw new Error('unexpected');
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const detected = await detectTradeMarketType(publicClient, MARKET);
|
|
22
|
+
assert.equal(detected.marketType, 'parimutuel');
|
|
23
|
+
assert.equal(detected.detectedBy, 'curveFlattener');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('detectTradeMarketType falls back to amm marker', async () => {
|
|
27
|
+
const publicClient = {
|
|
28
|
+
async readContract(request) {
|
|
29
|
+
if (request.functionName === 'curveFlattener') throw new Error('function selector was not recognized');
|
|
30
|
+
if (request.functionName === 'tradingFee') return 3000n;
|
|
31
|
+
throw new Error('unexpected');
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const detected = await detectTradeMarketType(publicClient, MARKET);
|
|
36
|
+
assert.equal(detected.marketType, 'amm');
|
|
37
|
+
assert.equal(detected.detectedBy, 'tradingFee');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('detectTradeMarketType throws unsupported interface when neither marker exists', async () => {
|
|
41
|
+
const publicClient = {
|
|
42
|
+
async readContract() {
|
|
43
|
+
throw new Error('reverted');
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
await assert.rejects(
|
|
48
|
+
() => detectTradeMarketType(publicClient, MARKET),
|
|
49
|
+
(error) => {
|
|
50
|
+
assert.equal(error.code, 'UNSUPPORTED_MARKET_TRADE_INTERFACE');
|
|
51
|
+
assert.equal(error.details.marketAddress, MARKET);
|
|
52
|
+
return true;
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('buildTradeBuyCall uses 3-arg buy for parimutuel', () => {
|
|
58
|
+
const call = buildTradeBuyCall({
|
|
59
|
+
marketType: 'parimutuel',
|
|
60
|
+
side: 'yes',
|
|
61
|
+
amountRaw: 1_000_000n,
|
|
62
|
+
minSharesOutRaw: 0n,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
assert.equal(call.signature, 'buy(bool,uint256,uint256)');
|
|
66
|
+
assert.deepEqual(call.args, [true, 1_000_000n, 0n]);
|
|
67
|
+
assert.equal(call.ammDeadlineEpoch, null);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('buildTradeBuyCall uses 4-arg deadline buy for amm', () => {
|
|
71
|
+
const nowEpochSec = 1_700_000_000;
|
|
72
|
+
const call = buildTradeBuyCall({
|
|
73
|
+
marketType: 'amm',
|
|
74
|
+
side: 'no',
|
|
75
|
+
amountRaw: 2_500_000n,
|
|
76
|
+
minSharesOutRaw: 12n,
|
|
77
|
+
nowEpochSec,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
assert.equal(call.signature, 'buy(bool,uint256,uint256,uint256)');
|
|
81
|
+
assert.equal(call.args.length, 4);
|
|
82
|
+
assert.deepEqual(call.args.slice(0, 3), [false, 2_500_000n, 12n]);
|
|
83
|
+
assert.equal(call.ammDeadlineEpoch, String(nowEpochSec + DEFAULT_AMM_TRADE_DEADLINE_OFFSET_SEC));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('resolveTradeBuyCall composes detection and call creation', async () => {
|
|
87
|
+
const publicClient = {
|
|
88
|
+
async readContract(request) {
|
|
89
|
+
if (request.functionName === 'curveFlattener') throw new Error('not pari');
|
|
90
|
+
if (request.functionName === 'tradingFee') return 500n;
|
|
91
|
+
throw new Error('unexpected');
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const call = await resolveTradeBuyCall({
|
|
96
|
+
publicClient,
|
|
97
|
+
marketAddress: MARKET,
|
|
98
|
+
side: 'yes',
|
|
99
|
+
amountRaw: 9n,
|
|
100
|
+
minSharesOutRaw: 1n,
|
|
101
|
+
nowEpochSec: 1000,
|
|
102
|
+
ammDeadlineOffsetSec: 5,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
assert.equal(call.marketType, 'amm');
|
|
106
|
+
assert.equal(call.detectedBy, 'tradingFee');
|
|
107
|
+
assert.equal(call.signature, 'buy(bool,uint256,uint256,uint256)');
|
|
108
|
+
assert.deepEqual(call.args, [true, 9n, 1n, 1005n]);
|
|
109
|
+
});
|