pandora-cli-skills 1.1.12 → 1.1.13
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/cli/lib/contract_error_decoder.cjs +138 -0
- package/cli/lib/market_admin_service.cjs +868 -0
- package/cli/lib/mirror_service.cjs +1 -0
- package/cli/lib/mirror_sync_service.cjs +7 -1
- package/cli/lib/pandora_deploy_service.cjs +68 -26
- package/cli/lib/polymarket_trade_adapter.cjs +6 -1
- package/cli/pandora.cjs +514 -58
- package/package.json +1 -1
- package/scripts/.env.example +2 -0
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
const DEFAULT_INDEXER_URL = 'https://pandoraindexer.up.railway.app/';
|
|
2
|
+
const DEFAULT_RPC_BY_CHAIN_ID = {
|
|
3
|
+
1: 'https://ethereum.publicnode.com',
|
|
4
|
+
146: 'https://rpc.soniclabs.com',
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const ERC20_ABI = [
|
|
8
|
+
{
|
|
9
|
+
type: 'function',
|
|
10
|
+
name: 'allowance',
|
|
11
|
+
stateMutability: 'view',
|
|
12
|
+
inputs: [
|
|
13
|
+
{ name: 'owner', type: 'address' },
|
|
14
|
+
{ name: 'spender', type: 'address' },
|
|
15
|
+
],
|
|
16
|
+
outputs: [{ type: 'uint256' }],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
type: 'function',
|
|
20
|
+
name: 'approve',
|
|
21
|
+
stateMutability: 'nonpayable',
|
|
22
|
+
inputs: [
|
|
23
|
+
{ name: 'spender', type: 'address' },
|
|
24
|
+
{ name: 'amount', type: 'uint256' },
|
|
25
|
+
],
|
|
26
|
+
outputs: [{ type: 'bool' }],
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const LP_TOKEN_ABI = [
|
|
31
|
+
{
|
|
32
|
+
type: 'function',
|
|
33
|
+
name: 'balanceOf',
|
|
34
|
+
stateMutability: 'view',
|
|
35
|
+
inputs: [{ name: 'owner', type: 'address' }],
|
|
36
|
+
outputs: [{ type: 'uint256' }],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: 'function',
|
|
40
|
+
name: 'decimals',
|
|
41
|
+
stateMutability: 'view',
|
|
42
|
+
inputs: [],
|
|
43
|
+
outputs: [{ type: 'uint8' }],
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const PREDICTION_AMM_ABI = [
|
|
48
|
+
{
|
|
49
|
+
type: 'function',
|
|
50
|
+
name: 'addLiquidity',
|
|
51
|
+
stateMutability: 'nonpayable',
|
|
52
|
+
inputs: [
|
|
53
|
+
{ name: 'collateralAmount', type: 'uint256' },
|
|
54
|
+
{ name: 'minOutcomeShares', type: 'uint256[2]' },
|
|
55
|
+
{ name: 'deadline', type: 'uint256' },
|
|
56
|
+
],
|
|
57
|
+
outputs: [{ type: 'uint256' }],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
type: 'function',
|
|
61
|
+
name: 'removeLiquidity',
|
|
62
|
+
stateMutability: 'nonpayable',
|
|
63
|
+
inputs: [
|
|
64
|
+
{ name: 'sharesToBurn', type: 'uint256' },
|
|
65
|
+
{ name: 'minCollateralOut', type: 'uint256' },
|
|
66
|
+
{ name: 'deadline', type: 'uint256' },
|
|
67
|
+
],
|
|
68
|
+
outputs: [{ type: 'uint256' }],
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const CALC_REMOVE_LIQUIDITY_ABI_CANDIDATES = [
|
|
73
|
+
[
|
|
74
|
+
{
|
|
75
|
+
type: 'function',
|
|
76
|
+
name: 'calcRemoveLiquidity',
|
|
77
|
+
stateMutability: 'view',
|
|
78
|
+
inputs: [{ name: 'sharesToBurn', type: 'uint256' }],
|
|
79
|
+
outputs: [
|
|
80
|
+
{ name: 'collateralOut', type: 'uint256' },
|
|
81
|
+
{ name: 'yesOut', type: 'uint256' },
|
|
82
|
+
{ name: 'noOut', type: 'uint256' },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
{
|
|
88
|
+
type: 'function',
|
|
89
|
+
name: 'calcRemoveLiquidity',
|
|
90
|
+
stateMutability: 'view',
|
|
91
|
+
inputs: [{ name: 'sharesToBurn', type: 'uint256' }],
|
|
92
|
+
outputs: [
|
|
93
|
+
{ name: 'collateralOut', type: 'uint256' },
|
|
94
|
+
{ name: 'yesOut', type: 'uint256' },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
[
|
|
99
|
+
{
|
|
100
|
+
type: 'function',
|
|
101
|
+
name: 'calcRemoveLiquidity',
|
|
102
|
+
stateMutability: 'view',
|
|
103
|
+
inputs: [{ name: 'sharesToBurn', type: 'uint256' }],
|
|
104
|
+
outputs: [{ name: 'collateralOut', type: 'uint256' }],
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
const RESOLVE_MARKET_ABI = [
|
|
110
|
+
{
|
|
111
|
+
type: 'function',
|
|
112
|
+
name: 'resolveMarket',
|
|
113
|
+
stateMutability: 'nonpayable',
|
|
114
|
+
inputs: [{ name: 'outcome', type: 'bool' }],
|
|
115
|
+
outputs: [],
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
function createServiceError(code, message, details = undefined) {
|
|
120
|
+
const err = new Error(message);
|
|
121
|
+
err.code = code;
|
|
122
|
+
if (details !== undefined) {
|
|
123
|
+
err.details = details;
|
|
124
|
+
}
|
|
125
|
+
return err;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isValidHttpUrl(value) {
|
|
129
|
+
try {
|
|
130
|
+
const parsed = new URL(String(value || ''));
|
|
131
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
132
|
+
} catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function buildChain(chainId, rpcUrl) {
|
|
138
|
+
if (chainId === 1) {
|
|
139
|
+
return {
|
|
140
|
+
id: 1,
|
|
141
|
+
name: 'Ethereum',
|
|
142
|
+
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
|
|
143
|
+
rpcUrls: { default: { http: [rpcUrl] }, public: { http: [rpcUrl] } },
|
|
144
|
+
blockExplorers: { default: { name: 'Etherscan', url: 'https://etherscan.io' } },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (chainId === 146) {
|
|
149
|
+
return {
|
|
150
|
+
id: 146,
|
|
151
|
+
name: 'Sonic',
|
|
152
|
+
nativeCurrency: { name: 'Sonic', symbol: 'S', decimals: 18 },
|
|
153
|
+
rpcUrls: { default: { http: [rpcUrl] }, public: { http: [rpcUrl] } },
|
|
154
|
+
blockExplorers: { default: { name: 'SonicScan', url: 'https://sonicscan.org' } },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
throw createServiceError('INVALID_FLAG_VALUE', `Unsupported chain id ${chainId}. Supported values: 1, 146.`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function normalizeAddress(value, label) {
|
|
162
|
+
const raw = String(value || '').trim();
|
|
163
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(raw)) {
|
|
164
|
+
throw createServiceError('INVALID_FLAG_VALUE', `${label} must be a valid address.`);
|
|
165
|
+
}
|
|
166
|
+
return raw.toLowerCase();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function normalizePrivateKey(value, label = 'private key') {
|
|
170
|
+
const raw = String(value || '').trim();
|
|
171
|
+
if (!/^0x[a-fA-F0-9]{64}$/.test(raw)) {
|
|
172
|
+
throw createServiceError('INVALID_FLAG_VALUE', `Invalid ${label}. Expected 0x + 64 hex chars.`);
|
|
173
|
+
}
|
|
174
|
+
return raw;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function normalizeTimeoutMs(value) {
|
|
178
|
+
const numeric = Number(value);
|
|
179
|
+
if (!Number.isFinite(numeric) || numeric <= 0) return 12_000;
|
|
180
|
+
return Math.trunc(numeric);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function toFiniteNumber(value) {
|
|
184
|
+
const numeric = Number(value);
|
|
185
|
+
if (!Number.isFinite(numeric)) return null;
|
|
186
|
+
return numeric;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function toBigIntOrNull(value) {
|
|
190
|
+
if (value === null || value === undefined || value === '') return null;
|
|
191
|
+
if (typeof value === 'bigint') return value;
|
|
192
|
+
try {
|
|
193
|
+
return BigInt(value);
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function normalizeBigIntForJson(value) {
|
|
200
|
+
if (typeof value === 'bigint') return value.toString();
|
|
201
|
+
if (Array.isArray(value)) return value.map((item) => normalizeBigIntForJson(item));
|
|
202
|
+
if (value && typeof value === 'object') {
|
|
203
|
+
const output = {};
|
|
204
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
205
|
+
output[key] = normalizeBigIntForJson(nested);
|
|
206
|
+
}
|
|
207
|
+
return output;
|
|
208
|
+
}
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function normalizeResolveOutcome(answer) {
|
|
213
|
+
const normalized = String(answer || '').trim().toLowerCase();
|
|
214
|
+
if (normalized === 'yes') return true;
|
|
215
|
+
if (normalized === 'no') return false;
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function txExplorerUrl(chainId, txHash) {
|
|
220
|
+
if (!txHash) return null;
|
|
221
|
+
if (chainId === 1) return `https://etherscan.io/tx/${txHash}`;
|
|
222
|
+
if (chainId === 146) return `https://sonicscan.org/tx/${txHash}`;
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function loadViemRuntime() {
|
|
227
|
+
const viem = await import('viem');
|
|
228
|
+
const accounts = await import('viem/accounts');
|
|
229
|
+
return { ...viem, ...accounts };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function resolveRuntime(options = {}, runtimeOptions = {}) {
|
|
233
|
+
const chainId = Number(
|
|
234
|
+
options.chainId !== null && options.chainId !== undefined
|
|
235
|
+
? options.chainId
|
|
236
|
+
: process.env.CHAIN_ID || 1,
|
|
237
|
+
);
|
|
238
|
+
if (!Number.isInteger(chainId)) {
|
|
239
|
+
throw createServiceError('INVALID_FLAG_VALUE', 'CHAIN_ID must be an integer.');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const rpcUrl = String(
|
|
243
|
+
options.rpcUrl || process.env.RPC_URL || DEFAULT_RPC_BY_CHAIN_ID[chainId] || '',
|
|
244
|
+
).trim();
|
|
245
|
+
if (!isValidHttpUrl(rpcUrl)) {
|
|
246
|
+
throw createServiceError('INVALID_FLAG_VALUE', `RPC URL must be a valid http/https URL. Received: "${rpcUrl}"`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const chain = buildChain(chainId, rpcUrl);
|
|
250
|
+
const runtime = {
|
|
251
|
+
chainId,
|
|
252
|
+
rpcUrl,
|
|
253
|
+
chain,
|
|
254
|
+
privateKey: null,
|
|
255
|
+
usdc: null,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
if (runtimeOptions.requirePrivateKey) {
|
|
259
|
+
runtime.privateKey = normalizePrivateKey(
|
|
260
|
+
options.privateKey || process.env.PRIVATE_KEY || process.env.DEPLOYER_PRIVATE_KEY,
|
|
261
|
+
'private key',
|
|
262
|
+
);
|
|
263
|
+
} else if (options.privateKey || process.env.PRIVATE_KEY || process.env.DEPLOYER_PRIVATE_KEY) {
|
|
264
|
+
runtime.privateKey = normalizePrivateKey(
|
|
265
|
+
options.privateKey || process.env.PRIVATE_KEY || process.env.DEPLOYER_PRIVATE_KEY,
|
|
266
|
+
'private key',
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (runtimeOptions.requireUsdc) {
|
|
271
|
+
runtime.usdc = normalizeAddress(options.usdc || process.env.USDC, 'USDC');
|
|
272
|
+
} else if (options.usdc || process.env.USDC) {
|
|
273
|
+
runtime.usdc = normalizeAddress(options.usdc || process.env.USDC, 'USDC');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return runtime;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function createClients(runtime, requireWallet = false) {
|
|
280
|
+
const { createPublicClient, createWalletClient, http, privateKeyToAccount } = await loadViemRuntime();
|
|
281
|
+
const publicClient = createPublicClient({ chain: runtime.chain, transport: http(runtime.rpcUrl) });
|
|
282
|
+
let account = null;
|
|
283
|
+
let walletClient = null;
|
|
284
|
+
|
|
285
|
+
if (runtime.privateKey) {
|
|
286
|
+
account = privateKeyToAccount(runtime.privateKey);
|
|
287
|
+
walletClient = createWalletClient({ account, chain: runtime.chain, transport: http(runtime.rpcUrl) });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (requireWallet && (!account || !walletClient)) {
|
|
291
|
+
throw createServiceError('MISSING_REQUIRED_FLAG', 'Missing private key. Set PRIVATE_KEY or pass --private-key.');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return { publicClient, walletClient, account };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function hasBytecode(code) {
|
|
298
|
+
const normalized = String(code || '').trim().toLowerCase();
|
|
299
|
+
return normalized !== '0x' && normalized !== '0x0' && normalized.length > 2;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function ensureContractCode(publicClient, address, label) {
|
|
303
|
+
const code = await publicClient.getBytecode({ address });
|
|
304
|
+
if (!hasBytecode(code)) {
|
|
305
|
+
throw createServiceError('MARKET_ADDRESS_NO_CODE', `${label} has no bytecode: ${address}`, {
|
|
306
|
+
address,
|
|
307
|
+
label,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function decodeAndWrapError(err, fallbackCode, fallbackMessage) {
|
|
313
|
+
const { decodeContractError, formatDecodedContractError } = require('./contract_error_decoder.cjs');
|
|
314
|
+
const decoded = await decodeContractError(err);
|
|
315
|
+
const decodedMessage = formatDecodedContractError(decoded);
|
|
316
|
+
const message = decodedMessage || (err && err.message ? err.message : fallbackMessage);
|
|
317
|
+
return createServiceError(fallbackCode, message, {
|
|
318
|
+
decoded,
|
|
319
|
+
cause: err && err.message ? err.message : String(err),
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function readDecimals(publicClient, address, fallback = 18) {
|
|
324
|
+
try {
|
|
325
|
+
const value = await publicClient.readContract({
|
|
326
|
+
address,
|
|
327
|
+
abi: LP_TOKEN_ABI,
|
|
328
|
+
functionName: 'decimals',
|
|
329
|
+
args: [],
|
|
330
|
+
});
|
|
331
|
+
const numeric = Number(value);
|
|
332
|
+
if (!Number.isInteger(numeric) || numeric < 0 || numeric > 36) return fallback;
|
|
333
|
+
return numeric;
|
|
334
|
+
} catch {
|
|
335
|
+
return fallback;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function readCalcRemoveLiquidity(publicClient, marketAddress, sharesRaw) {
|
|
340
|
+
for (const abi of CALC_REMOVE_LIQUIDITY_ABI_CANDIDATES) {
|
|
341
|
+
try {
|
|
342
|
+
const value = await publicClient.readContract({
|
|
343
|
+
address: marketAddress,
|
|
344
|
+
abi,
|
|
345
|
+
functionName: 'calcRemoveLiquidity',
|
|
346
|
+
args: [sharesRaw],
|
|
347
|
+
});
|
|
348
|
+
const normalized = Array.isArray(value) ? value : [value];
|
|
349
|
+
return {
|
|
350
|
+
collateralOutRaw: normalized[0] ? normalized[0].toString() : null,
|
|
351
|
+
yesOutRaw: normalized[1] ? normalized[1].toString() : null,
|
|
352
|
+
noOutRaw: normalized[2] ? normalized[2].toString() : null,
|
|
353
|
+
};
|
|
354
|
+
} catch {
|
|
355
|
+
// try next ABI candidate
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function graphqlRequest(indexerUrl, query, variables, timeoutMs) {
|
|
362
|
+
const controller = new AbortController();
|
|
363
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
364
|
+
try {
|
|
365
|
+
const response = await fetch(indexerUrl, {
|
|
366
|
+
method: 'POST',
|
|
367
|
+
headers: { 'content-type': 'application/json' },
|
|
368
|
+
body: JSON.stringify({ query, variables }),
|
|
369
|
+
signal: controller.signal,
|
|
370
|
+
});
|
|
371
|
+
if (!response.ok) {
|
|
372
|
+
throw createServiceError('INDEXER_HTTP_ERROR', `Indexer returned HTTP ${response.status}.`);
|
|
373
|
+
}
|
|
374
|
+
const payload = await response.json();
|
|
375
|
+
if (Array.isArray(payload.errors) && payload.errors.length) {
|
|
376
|
+
throw createServiceError('INDEXER_QUERY_FAILED', payload.errors[0].message || 'Indexer query failed.');
|
|
377
|
+
}
|
|
378
|
+
return payload.data || {};
|
|
379
|
+
} finally {
|
|
380
|
+
clearTimeout(timeout);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function fetchIndexerMarket(indexerUrl, marketAddress, timeoutMs) {
|
|
385
|
+
const query = `
|
|
386
|
+
query($id: String!) {
|
|
387
|
+
markets(id: $id) {
|
|
388
|
+
id
|
|
389
|
+
pollAddress
|
|
390
|
+
chainId
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
`;
|
|
394
|
+
const data = await graphqlRequest(indexerUrl, query, { id: marketAddress }, timeoutMs);
|
|
395
|
+
return data.markets || null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function discoverLiquidityMarkets(indexerUrl, wallet, chainId, timeoutMs) {
|
|
399
|
+
const query = `
|
|
400
|
+
query($where: liquidityEventsFilter, $limit: Int) {
|
|
401
|
+
liquidityEventss(where: $where, orderBy: "timestamp", orderDirection: "desc", limit: $limit) {
|
|
402
|
+
items {
|
|
403
|
+
marketAddress
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
`;
|
|
408
|
+
const where = { provider: wallet };
|
|
409
|
+
if (Number.isInteger(chainId)) {
|
|
410
|
+
where.chainId = chainId;
|
|
411
|
+
}
|
|
412
|
+
const data = await graphqlRequest(indexerUrl, query, { where, limit: 500 }, timeoutMs);
|
|
413
|
+
const page = data.liquidityEventss;
|
|
414
|
+
const items = page && Array.isArray(page.items) ? page.items : [];
|
|
415
|
+
const addresses = items
|
|
416
|
+
.map((item) => String(item && item.marketAddress ? item.marketAddress : '').toLowerCase())
|
|
417
|
+
.filter((value) => /^0x[a-f0-9]{40}$/.test(value));
|
|
418
|
+
return Array.from(new Set(addresses));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function runResolve(options = {}) {
|
|
422
|
+
const schemaVersion = '1.0.0';
|
|
423
|
+
const generatedAt = new Date().toISOString();
|
|
424
|
+
const outcome = normalizeResolveOutcome(options.answer);
|
|
425
|
+
if (outcome === null) {
|
|
426
|
+
throw createServiceError(
|
|
427
|
+
'INVALID_FLAG_VALUE',
|
|
428
|
+
'--answer invalid is not supported by resolveMarket(bool). Use yes|no.',
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const deadlineSeconds = Number.isInteger(Number(options.deadlineSeconds))
|
|
433
|
+
? Math.max(60, Math.trunc(Number(options.deadlineSeconds)))
|
|
434
|
+
: 1800;
|
|
435
|
+
const payload = {
|
|
436
|
+
schemaVersion,
|
|
437
|
+
generatedAt,
|
|
438
|
+
mode: options.execute ? 'execute' : 'dry-run',
|
|
439
|
+
status: options.execute ? 'submitted' : 'planned',
|
|
440
|
+
pollAddress: options.pollAddress,
|
|
441
|
+
answer: options.answer,
|
|
442
|
+
reason: options.reason,
|
|
443
|
+
txPlan: {
|
|
444
|
+
functionName: 'resolveMarket',
|
|
445
|
+
args: [outcome],
|
|
446
|
+
abiSignature: 'resolveMarket(bool)',
|
|
447
|
+
notes: [
|
|
448
|
+
'Resolution is restricted by arbiter/operator checks on-chain.',
|
|
449
|
+
`Reason is recorded off-chain in CLI payload: ${options.reason}`,
|
|
450
|
+
],
|
|
451
|
+
},
|
|
452
|
+
tx: null,
|
|
453
|
+
diagnostics: [],
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
if (!options.execute) {
|
|
457
|
+
return payload;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const runtime = await resolveRuntime(options, { requirePrivateKey: true, requireUsdc: false });
|
|
461
|
+
const { publicClient, walletClient, account } = await createClients(runtime, true);
|
|
462
|
+
const pollAddress = normalizeAddress(options.pollAddress, 'pollAddress');
|
|
463
|
+
|
|
464
|
+
await ensureContractCode(publicClient, pollAddress, 'Poll contract');
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
const simulation = await publicClient.simulateContract({
|
|
468
|
+
account,
|
|
469
|
+
address: pollAddress,
|
|
470
|
+
abi: RESOLVE_MARKET_ABI,
|
|
471
|
+
functionName: 'resolveMarket',
|
|
472
|
+
args: [outcome],
|
|
473
|
+
});
|
|
474
|
+
const txHash = await walletClient.writeContract(simulation.request);
|
|
475
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
476
|
+
payload.tx = {
|
|
477
|
+
chainId: runtime.chainId,
|
|
478
|
+
account: account.address,
|
|
479
|
+
txHash,
|
|
480
|
+
explorerUrl: txExplorerUrl(runtime.chainId, txHash),
|
|
481
|
+
gasEstimate: simulation.request && simulation.request.gas ? simulation.request.gas.toString() : null,
|
|
482
|
+
status: receipt && receipt.status ? receipt.status : null,
|
|
483
|
+
blockNumber:
|
|
484
|
+
receipt && receipt.blockNumber !== undefined && receipt.blockNumber !== null
|
|
485
|
+
? receipt.blockNumber.toString()
|
|
486
|
+
: null,
|
|
487
|
+
};
|
|
488
|
+
return payload;
|
|
489
|
+
} catch (err) {
|
|
490
|
+
throw await decodeAndWrapError(err, 'RESOLVE_EXECUTION_FAILED', 'Failed to execute resolveMarket.');
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function runLpPositions(options = {}) {
|
|
495
|
+
const schemaVersion = '1.0.0';
|
|
496
|
+
const generatedAt = new Date().toISOString();
|
|
497
|
+
const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
|
|
498
|
+
const runtime = await resolveRuntime(options, { requirePrivateKey: false, requireUsdc: false });
|
|
499
|
+
const { publicClient } = await createClients(runtime, false);
|
|
500
|
+
const wallet = normalizeAddress(options.wallet, '--wallet');
|
|
501
|
+
const diagnostics = [];
|
|
502
|
+
|
|
503
|
+
let markets = [];
|
|
504
|
+
if (options.marketAddress) {
|
|
505
|
+
markets = [normalizeAddress(options.marketAddress, '--market-address')];
|
|
506
|
+
} else {
|
|
507
|
+
const indexerUrl = options.indexerUrl || process.env.PANDORA_INDEXER_URL || process.env.INDEXER_URL || DEFAULT_INDEXER_URL;
|
|
508
|
+
try {
|
|
509
|
+
markets = await discoverLiquidityMarkets(indexerUrl, wallet, options.chainId || runtime.chainId, timeoutMs);
|
|
510
|
+
if (!markets.length) {
|
|
511
|
+
diagnostics.push('No LP markets discovered from indexer liquidity events for this wallet.');
|
|
512
|
+
}
|
|
513
|
+
} catch (err) {
|
|
514
|
+
diagnostics.push(`Indexer market discovery failed: ${err && err.message ? err.message : String(err)}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const { formatUnits } = await loadViemRuntime();
|
|
519
|
+
const items = [];
|
|
520
|
+
for (const marketAddress of markets) {
|
|
521
|
+
const itemDiagnostics = [];
|
|
522
|
+
try {
|
|
523
|
+
await ensureContractCode(publicClient, marketAddress, 'Market');
|
|
524
|
+
} catch (err) {
|
|
525
|
+
itemDiagnostics.push(err.message || String(err));
|
|
526
|
+
items.push({
|
|
527
|
+
marketAddress,
|
|
528
|
+
lpTokenDecimals: null,
|
|
529
|
+
lpTokenBalanceRaw: null,
|
|
530
|
+
lpTokenBalance: null,
|
|
531
|
+
preview: null,
|
|
532
|
+
diagnostics: itemDiagnostics,
|
|
533
|
+
});
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const lpTokenDecimals = await readDecimals(publicClient, marketAddress, 18);
|
|
538
|
+
let lpTokenBalanceRaw = null;
|
|
539
|
+
try {
|
|
540
|
+
const value = await publicClient.readContract({
|
|
541
|
+
address: marketAddress,
|
|
542
|
+
abi: LP_TOKEN_ABI,
|
|
543
|
+
functionName: 'balanceOf',
|
|
544
|
+
args: [wallet],
|
|
545
|
+
});
|
|
546
|
+
lpTokenBalanceRaw = value;
|
|
547
|
+
} catch (err) {
|
|
548
|
+
itemDiagnostics.push(`balanceOf failed: ${err && err.message ? err.message : String(err)}`);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
let preview = null;
|
|
552
|
+
if (typeof lpTokenBalanceRaw === 'bigint' && lpTokenBalanceRaw > 0n) {
|
|
553
|
+
const calc = await readCalcRemoveLiquidity(publicClient, marketAddress, lpTokenBalanceRaw);
|
|
554
|
+
if (calc) {
|
|
555
|
+
preview = {
|
|
556
|
+
collateralOutRaw: calc.collateralOutRaw,
|
|
557
|
+
collateralOutUsdc:
|
|
558
|
+
calc.collateralOutRaw !== null ? formatUnits(BigInt(calc.collateralOutRaw), 6) : null,
|
|
559
|
+
yesOutRaw: calc.yesOutRaw,
|
|
560
|
+
yesOut:
|
|
561
|
+
calc.yesOutRaw !== null ? formatUnits(BigInt(calc.yesOutRaw), 18) : null,
|
|
562
|
+
noOutRaw: calc.noOutRaw,
|
|
563
|
+
noOut:
|
|
564
|
+
calc.noOutRaw !== null ? formatUnits(BigInt(calc.noOutRaw), 18) : null,
|
|
565
|
+
};
|
|
566
|
+
} else {
|
|
567
|
+
itemDiagnostics.push('calcRemoveLiquidity unavailable for this market ABI.');
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
items.push({
|
|
572
|
+
marketAddress,
|
|
573
|
+
lpTokenDecimals,
|
|
574
|
+
lpTokenBalanceRaw: lpTokenBalanceRaw === null ? null : lpTokenBalanceRaw.toString(),
|
|
575
|
+
lpTokenBalance:
|
|
576
|
+
lpTokenBalanceRaw === null ? null : formatUnits(lpTokenBalanceRaw, lpTokenDecimals),
|
|
577
|
+
preview,
|
|
578
|
+
diagnostics: itemDiagnostics,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return {
|
|
583
|
+
schemaVersion,
|
|
584
|
+
generatedAt,
|
|
585
|
+
mode: 'read',
|
|
586
|
+
action: 'positions',
|
|
587
|
+
wallet,
|
|
588
|
+
chainId: runtime.chainId,
|
|
589
|
+
rpcUrl: runtime.rpcUrl,
|
|
590
|
+
count: items.length,
|
|
591
|
+
items,
|
|
592
|
+
diagnostics,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async function runLpAdd(options = {}) {
|
|
597
|
+
const schemaVersion = '1.0.0';
|
|
598
|
+
const generatedAt = new Date().toISOString();
|
|
599
|
+
const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
|
|
600
|
+
const deadlineSeconds = Number.isInteger(Number(options.deadlineSeconds))
|
|
601
|
+
? Math.max(60, Math.trunc(Number(options.deadlineSeconds)))
|
|
602
|
+
: 1800;
|
|
603
|
+
|
|
604
|
+
const payload = {
|
|
605
|
+
schemaVersion,
|
|
606
|
+
generatedAt,
|
|
607
|
+
mode: options.execute ? 'execute' : 'dry-run',
|
|
608
|
+
status: options.execute ? 'submitted' : 'planned',
|
|
609
|
+
action: 'add',
|
|
610
|
+
marketAddress: options.marketAddress,
|
|
611
|
+
amountUsdc: options.amountUsdc,
|
|
612
|
+
deadlineSeconds,
|
|
613
|
+
txPlan: null,
|
|
614
|
+
preflight: null,
|
|
615
|
+
tx: null,
|
|
616
|
+
diagnostics: [],
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const runtime = await resolveRuntime(options, {
|
|
620
|
+
requirePrivateKey: options.execute,
|
|
621
|
+
requireUsdc: true,
|
|
622
|
+
});
|
|
623
|
+
const marketAddress = normalizeAddress(options.marketAddress, '--market-address');
|
|
624
|
+
|
|
625
|
+
const { parseUnits, formatUnits } = await loadViemRuntime();
|
|
626
|
+
const collateralAmountRaw = parseUnits(String(options.amountUsdc), 6);
|
|
627
|
+
const deadline = BigInt(Math.floor(Date.now() / 1000) + deadlineSeconds);
|
|
628
|
+
const minOutcomeShares = [0n, 0n];
|
|
629
|
+
|
|
630
|
+
payload.txPlan = {
|
|
631
|
+
collateralAmountRaw: collateralAmountRaw.toString(),
|
|
632
|
+
minOutcomeSharesRaw: minOutcomeShares.map((item) => item.toString()),
|
|
633
|
+
deadline: deadline.toString(),
|
|
634
|
+
removeLiquidityArgOrder: 'sharesToBurn, minCollateralOut, deadline',
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
if (!options.execute) {
|
|
638
|
+
return payload;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const { publicClient, walletClient, account } = await createClients(runtime, true);
|
|
642
|
+
await ensureContractCode(publicClient, marketAddress, 'Market');
|
|
643
|
+
|
|
644
|
+
let marketInIndexer = null;
|
|
645
|
+
const indexerUrl = options.indexerUrl || process.env.PANDORA_INDEXER_URL || process.env.INDEXER_URL || DEFAULT_INDEXER_URL;
|
|
646
|
+
try {
|
|
647
|
+
marketInIndexer = await fetchIndexerMarket(indexerUrl, marketAddress, timeoutMs);
|
|
648
|
+
if (!marketInIndexer) {
|
|
649
|
+
payload.diagnostics.push('Market address not found in indexer markets(). Verify the target market.');
|
|
650
|
+
}
|
|
651
|
+
} catch (err) {
|
|
652
|
+
payload.diagnostics.push(`Indexer market validation skipped: ${err && err.message ? err.message : String(err)}`);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
let allowance;
|
|
656
|
+
try {
|
|
657
|
+
allowance = await publicClient.readContract({
|
|
658
|
+
address: runtime.usdc,
|
|
659
|
+
abi: ERC20_ABI,
|
|
660
|
+
functionName: 'allowance',
|
|
661
|
+
args: [account.address, marketAddress],
|
|
662
|
+
});
|
|
663
|
+
} catch (err) {
|
|
664
|
+
throw await decodeAndWrapError(err, 'LP_ADD_ALLOWANCE_READ_FAILED', 'Failed to read USDC allowance.');
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const approveRequired = allowance < collateralAmountRaw;
|
|
668
|
+
const preflight = {
|
|
669
|
+
account: account.address,
|
|
670
|
+
chainId: runtime.chainId,
|
|
671
|
+
usdc: runtime.usdc,
|
|
672
|
+
allowanceRaw: allowance.toString(),
|
|
673
|
+
amountRaw: collateralAmountRaw.toString(),
|
|
674
|
+
allowanceSufficient: !approveRequired,
|
|
675
|
+
amountUsdc: formatUnits(collateralAmountRaw, 6),
|
|
676
|
+
marketInIndexer: Boolean(marketInIndexer),
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
let approveSimulation = null;
|
|
680
|
+
if (approveRequired) {
|
|
681
|
+
try {
|
|
682
|
+
approveSimulation = await publicClient.simulateContract({
|
|
683
|
+
account,
|
|
684
|
+
address: runtime.usdc,
|
|
685
|
+
abi: ERC20_ABI,
|
|
686
|
+
functionName: 'approve',
|
|
687
|
+
args: [marketAddress, collateralAmountRaw],
|
|
688
|
+
});
|
|
689
|
+
} catch (err) {
|
|
690
|
+
throw await decodeAndWrapError(err, 'LP_ADD_APPROVE_SIMULATION_FAILED', 'USDC approve simulation failed.');
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
let addSimulation;
|
|
695
|
+
try {
|
|
696
|
+
addSimulation = await publicClient.simulateContract({
|
|
697
|
+
account,
|
|
698
|
+
address: marketAddress,
|
|
699
|
+
abi: PREDICTION_AMM_ABI,
|
|
700
|
+
functionName: 'addLiquidity',
|
|
701
|
+
args: [collateralAmountRaw, minOutcomeShares, deadline],
|
|
702
|
+
});
|
|
703
|
+
} catch (err) {
|
|
704
|
+
throw await decodeAndWrapError(err, 'LP_ADD_SIMULATION_FAILED', 'addLiquidity simulation failed.');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
preflight.approveGasEstimate =
|
|
708
|
+
approveSimulation && approveSimulation.request && approveSimulation.request.gas
|
|
709
|
+
? approveSimulation.request.gas.toString()
|
|
710
|
+
: null;
|
|
711
|
+
preflight.addLiquidityGasEstimate =
|
|
712
|
+
addSimulation && addSimulation.request && addSimulation.request.gas
|
|
713
|
+
? addSimulation.request.gas.toString()
|
|
714
|
+
: null;
|
|
715
|
+
payload.preflight = preflight;
|
|
716
|
+
|
|
717
|
+
try {
|
|
718
|
+
let approveTxHash = null;
|
|
719
|
+
let approveReceipt = null;
|
|
720
|
+
if (approveRequired) {
|
|
721
|
+
approveTxHash = await walletClient.writeContract(approveSimulation.request);
|
|
722
|
+
approveReceipt = await publicClient.waitForTransactionReceipt({ hash: approveTxHash });
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const addTxHash = await walletClient.writeContract(addSimulation.request);
|
|
726
|
+
const addReceipt = await publicClient.waitForTransactionReceipt({ hash: addTxHash });
|
|
727
|
+
payload.tx = {
|
|
728
|
+
approveTxHash,
|
|
729
|
+
approveTxUrl: txExplorerUrl(runtime.chainId, approveTxHash),
|
|
730
|
+
approveStatus: approveReceipt && approveReceipt.status ? approveReceipt.status : null,
|
|
731
|
+
addTxHash,
|
|
732
|
+
addTxUrl: txExplorerUrl(runtime.chainId, addTxHash),
|
|
733
|
+
addStatus: addReceipt && addReceipt.status ? addReceipt.status : null,
|
|
734
|
+
addBlockNumber:
|
|
735
|
+
addReceipt && addReceipt.blockNumber !== undefined && addReceipt.blockNumber !== null
|
|
736
|
+
? addReceipt.blockNumber.toString()
|
|
737
|
+
: null,
|
|
738
|
+
};
|
|
739
|
+
return payload;
|
|
740
|
+
} catch (err) {
|
|
741
|
+
throw await decodeAndWrapError(err, 'LP_ADD_EXECUTION_FAILED', 'Failed to execute addLiquidity.');
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
async function runLpRemove(options = {}) {
|
|
746
|
+
const schemaVersion = '1.0.0';
|
|
747
|
+
const generatedAt = new Date().toISOString();
|
|
748
|
+
const deadlineSeconds = Number.isInteger(Number(options.deadlineSeconds))
|
|
749
|
+
? Math.max(60, Math.trunc(Number(options.deadlineSeconds)))
|
|
750
|
+
: 1800;
|
|
751
|
+
|
|
752
|
+
const payload = {
|
|
753
|
+
schemaVersion,
|
|
754
|
+
generatedAt,
|
|
755
|
+
mode: options.execute ? 'execute' : 'dry-run',
|
|
756
|
+
status: options.execute ? 'submitted' : 'planned',
|
|
757
|
+
action: 'remove',
|
|
758
|
+
marketAddress: options.marketAddress,
|
|
759
|
+
lpTokens: options.lpTokens,
|
|
760
|
+
deadlineSeconds,
|
|
761
|
+
txPlan: null,
|
|
762
|
+
preflight: null,
|
|
763
|
+
tx: null,
|
|
764
|
+
diagnostics: [],
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
const runtime = await resolveRuntime(options, {
|
|
768
|
+
requirePrivateKey: options.execute,
|
|
769
|
+
requireUsdc: false,
|
|
770
|
+
});
|
|
771
|
+
const marketAddress = normalizeAddress(options.marketAddress, '--market-address');
|
|
772
|
+
const { parseUnits, formatUnits } = await loadViemRuntime();
|
|
773
|
+
|
|
774
|
+
if (!options.execute) {
|
|
775
|
+
const deadline = BigInt(Math.floor(Date.now() / 1000) + deadlineSeconds);
|
|
776
|
+
payload.txPlan = {
|
|
777
|
+
lpTokenDecimalsAssumed: 18,
|
|
778
|
+
sharesToBurnRaw: parseUnits(String(options.lpTokens), 18).toString(),
|
|
779
|
+
minCollateralOutRaw: '0',
|
|
780
|
+
deadline: deadline.toString(),
|
|
781
|
+
removeLiquidityArgOrder: 'sharesToBurn, minCollateralOut, deadline',
|
|
782
|
+
};
|
|
783
|
+
return payload;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const { publicClient, walletClient, account } = await createClients(runtime, true);
|
|
787
|
+
await ensureContractCode(publicClient, marketAddress, 'Market');
|
|
788
|
+
|
|
789
|
+
const lpTokenDecimals = await readDecimals(publicClient, marketAddress, 18);
|
|
790
|
+
const sharesToBurnRaw = parseUnits(String(options.lpTokens), lpTokenDecimals);
|
|
791
|
+
const deadline = BigInt(Math.floor(Date.now() / 1000) + deadlineSeconds);
|
|
792
|
+
const minCollateralOutRaw = 0n;
|
|
793
|
+
const preview = await readCalcRemoveLiquidity(publicClient, marketAddress, sharesToBurnRaw);
|
|
794
|
+
|
|
795
|
+
payload.txPlan = {
|
|
796
|
+
lpTokenDecimals,
|
|
797
|
+
sharesToBurnRaw: sharesToBurnRaw.toString(),
|
|
798
|
+
minCollateralOutRaw: minCollateralOutRaw.toString(),
|
|
799
|
+
deadline: deadline.toString(),
|
|
800
|
+
removeLiquidityArgOrder: 'sharesToBurn, minCollateralOut, deadline',
|
|
801
|
+
};
|
|
802
|
+
payload.preflight = {
|
|
803
|
+
account: account.address,
|
|
804
|
+
chainId: runtime.chainId,
|
|
805
|
+
preview: preview
|
|
806
|
+
? {
|
|
807
|
+
collateralOutRaw: preview.collateralOutRaw,
|
|
808
|
+
collateralOutUsdc:
|
|
809
|
+
preview.collateralOutRaw !== null ? formatUnits(BigInt(preview.collateralOutRaw), 6) : null,
|
|
810
|
+
yesOutRaw: preview.yesOutRaw,
|
|
811
|
+
noOutRaw: preview.noOutRaw,
|
|
812
|
+
}
|
|
813
|
+
: null,
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
let simulation;
|
|
817
|
+
try {
|
|
818
|
+
simulation = await publicClient.simulateContract({
|
|
819
|
+
account,
|
|
820
|
+
address: marketAddress,
|
|
821
|
+
abi: PREDICTION_AMM_ABI,
|
|
822
|
+
functionName: 'removeLiquidity',
|
|
823
|
+
args: [sharesToBurnRaw, minCollateralOutRaw, deadline],
|
|
824
|
+
});
|
|
825
|
+
} catch (err) {
|
|
826
|
+
throw await decodeAndWrapError(err, 'LP_REMOVE_SIMULATION_FAILED', 'removeLiquidity simulation failed.');
|
|
827
|
+
}
|
|
828
|
+
payload.preflight.removeLiquidityGasEstimate =
|
|
829
|
+
simulation && simulation.request && simulation.request.gas
|
|
830
|
+
? simulation.request.gas.toString()
|
|
831
|
+
: null;
|
|
832
|
+
|
|
833
|
+
try {
|
|
834
|
+
const txHash = await walletClient.writeContract(simulation.request);
|
|
835
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
836
|
+
payload.tx = {
|
|
837
|
+
txHash,
|
|
838
|
+
txUrl: txExplorerUrl(runtime.chainId, txHash),
|
|
839
|
+
status: receipt && receipt.status ? receipt.status : null,
|
|
840
|
+
blockNumber:
|
|
841
|
+
receipt && receipt.blockNumber !== undefined && receipt.blockNumber !== null
|
|
842
|
+
? receipt.blockNumber.toString()
|
|
843
|
+
: null,
|
|
844
|
+
};
|
|
845
|
+
return payload;
|
|
846
|
+
} catch (err) {
|
|
847
|
+
throw await decodeAndWrapError(err, 'LP_REMOVE_EXECUTION_FAILED', 'Failed to execute removeLiquidity.');
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async function runLp(options = {}) {
|
|
852
|
+
if (options.action === 'positions') {
|
|
853
|
+
return runLpPositions(options);
|
|
854
|
+
}
|
|
855
|
+
if (options.action === 'add') {
|
|
856
|
+
return runLpAdd(options);
|
|
857
|
+
}
|
|
858
|
+
if (options.action === 'remove') {
|
|
859
|
+
return runLpRemove(options);
|
|
860
|
+
}
|
|
861
|
+
throw createServiceError('INVALID_ARGS', 'lp requires action add|remove|positions.');
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
module.exports = {
|
|
865
|
+
runResolve,
|
|
866
|
+
runLp,
|
|
867
|
+
runLpPositions,
|
|
868
|
+
};
|