@catalyst-team/poly-mcp 0.1.1 → 0.1.3
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 +264 -21
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +13 -2
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +98 -5
- package/dist/index.js.map +1 -1
- package/dist/sdk-instance.d.ts +27 -0
- package/dist/sdk-instance.d.ts.map +1 -0
- package/dist/sdk-instance.js +64 -0
- package/dist/sdk-instance.js.map +1 -0
- package/dist/server.d.ts +13 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +29 -27
- package/dist/server.js.map +1 -1
- package/dist/tools/guide.d.ts.map +1 -1
- package/dist/tools/guide.js +159 -1
- package/dist/tools/guide.js.map +1 -1
- package/dist/tools/index.d.ts +8 -4
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +20 -4
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/insider-detection.d.ts +175 -0
- package/dist/tools/insider-detection.d.ts.map +1 -0
- package/dist/tools/insider-detection.js +654 -0
- package/dist/tools/insider-detection.js.map +1 -0
- package/dist/tools/insider-signals.d.ts +56 -0
- package/dist/tools/insider-signals.d.ts.map +1 -0
- package/dist/tools/insider-signals.js +170 -0
- package/dist/tools/insider-signals.js.map +1 -0
- package/dist/tools/market.d.ts +25 -1
- package/dist/tools/market.d.ts.map +1 -1
- package/dist/tools/market.js +504 -12
- package/dist/tools/market.js.map +1 -1
- package/dist/tools/onchain.d.ts +240 -0
- package/dist/tools/onchain.d.ts.map +1 -0
- package/dist/tools/onchain.js +610 -0
- package/dist/tools/onchain.js.map +1 -0
- package/dist/tools/order.d.ts.map +1 -1
- package/dist/tools/order.js +13 -6
- package/dist/tools/order.js.map +1 -1
- package/dist/tools/trade.d.ts +15 -0
- package/dist/tools/trade.d.ts.map +1 -1
- package/dist/tools/trade.js +216 -39
- package/dist/tools/trade.js.map +1 -1
- package/dist/tools/trader.d.ts +4 -1
- package/dist/tools/trader.d.ts.map +1 -1
- package/dist/tools/trader.js +316 -4
- package/dist/tools/trader.js.map +1 -1
- package/dist/tools/wallet-classification.d.ts +166 -0
- package/dist/tools/wallet-classification.d.ts.map +1 -0
- package/dist/tools/wallet-classification.js +455 -0
- package/dist/tools/wallet-classification.js.map +1 -0
- package/dist/tools/wallet.d.ts +56 -7
- package/dist/tools/wallet.d.ts.map +1 -1
- package/dist/tools/wallet.js +141 -20
- package/dist/tools/wallet.js.map +1 -1
- package/dist/types.d.ts +269 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/wallet-manager.d.ts +67 -0
- package/dist/wallet-manager.d.ts.map +1 -0
- package/dist/wallet-manager.js +180 -0
- package/dist/wallet-manager.js.map +1 -0
- package/docs/01-mcp.md +554 -32
- package/docs/02-wallet-deep-research.md +344 -0
- package/docs/e2e-02/00-gap-analysis.md +211 -0
- package/docs/e2e-02/01-test-scenarios.md +530 -0
- package/docs/e2e-02/02-implementation-plan.md +190 -0
- package/docs/e2e-02/README.md +102 -0
- package/docs/reports/simonbanza-strategy-analysis-2025-12-25.md +420 -0
- package/docs/reports/smart-money-analysis-2025-12-23-cn.md +840 -0
- package/docs/reports/smart-money-trading-strategies-2025-12-25.md +440 -0
- package/docs/reports/weekly/01-v2.5.md +352 -0
- package/docs/reports/weekly/01.md +402 -0
- package/docs/reports/weekly/02-deep.md +558 -0
- package/docs/reports/weekly/02.md +505 -0
- package/docs/reports/weekly/03.md +437 -0
- package/docs/reports/weekly/04.md +418 -0
- package/docs/reports/weekly/05.md +485 -0
- package/docs/reports/weekly/06.md +436 -0
- package/docs/reports/weekly/07.md +381 -0
- package/docs/reports/weekly/08.md +502 -0
- package/docs/reports/weekly/09.md +441 -0
- package/docs/reports/weekly/10.md +511 -0
- package/docs/reports/weekly/README.md +188 -0
- package/docs/reports/weekly/prompt-v2.5.md +1019 -0
- package/docs/reports/weekly/prompt-v3.md +432 -0
- package/docs/reports/weekly/prompt.md +841 -0
- package/package.json +3 -2
- package/src/errors.ts +13 -2
- package/src/index.ts +286 -1
- package/src/sdk-instance.ts +78 -0
- package/src/server.ts +30 -28
- package/src/tools/guide.ts +160 -1
- package/src/tools/index.ts +65 -0
- package/src/tools/insider-detection.ts +899 -0
- package/src/tools/insider-signals.ts +213 -0
- package/src/tools/market.ts +569 -12
- package/src/tools/onchain.ts +738 -0
- package/src/tools/order.ts +25 -12
- package/src/tools/trade.ts +265 -53
- package/src/tools/trader.ts +350 -4
- package/src/tools/wallet-classification.ts +587 -0
- package/src/tools/wallet.ts +172 -23
- package/src/types.ts +294 -11
- package/src/wallet-manager.ts +209 -0
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onchain Tools - MCP tools for CTF (Conditional Token Framework) operations
|
|
3
|
+
*
|
|
4
|
+
* These tools use the OnchainService from poly-sdk:
|
|
5
|
+
* - Split: USDC -> YES + NO tokens
|
|
6
|
+
* - Merge: YES + NO tokens -> USDC
|
|
7
|
+
* - Redeem: Winning tokens -> USDC (after market resolution)
|
|
8
|
+
* - Position balance queries
|
|
9
|
+
* - Market resolution status
|
|
10
|
+
* - Gas estimation
|
|
11
|
+
*
|
|
12
|
+
* ## Token ID Auto-Discovery
|
|
13
|
+
*
|
|
14
|
+
* Polymarket CLOB markets use custom ERC-1155 token IDs that differ from
|
|
15
|
+
* standard CTF calculated positionIds. These tools auto-discover tokenIds
|
|
16
|
+
* from the CLOB API when not explicitly provided.
|
|
17
|
+
*
|
|
18
|
+
* ## Dynamic Outcome Names
|
|
19
|
+
*
|
|
20
|
+
* Markets can have different outcome names:
|
|
21
|
+
* - Yes/No (standard)
|
|
22
|
+
* - Up/Down (crypto short-term)
|
|
23
|
+
* - Team A/Team B (sports)
|
|
24
|
+
*
|
|
25
|
+
* The tools handle all outcome types dynamically.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
OnchainService,
|
|
30
|
+
type TokenIds,
|
|
31
|
+
type ResolvedMarketTokens,
|
|
32
|
+
} from '@catalyst-team/poly-sdk';
|
|
33
|
+
import type { ToolDefinition } from '../types.js';
|
|
34
|
+
import { wrapError, McpToolError, ErrorCode, validateConditionId } from '../errors.js';
|
|
35
|
+
import { getWalletManager } from '../wallet-manager.js';
|
|
36
|
+
import { getMarketService } from '../sdk-instance.js';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolved market tokens with tokenIds for CTF operations
|
|
40
|
+
*/
|
|
41
|
+
interface ResolvedTokens {
|
|
42
|
+
tokenIds: TokenIds;
|
|
43
|
+
primaryOutcome: string;
|
|
44
|
+
secondaryOutcome: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Auto-discover tokenIds using poly-sdk's MarketService
|
|
49
|
+
*
|
|
50
|
+
* Uses MarketService.resolveMarketTokens() which fetches actual tokenIds from CLOB API.
|
|
51
|
+
*
|
|
52
|
+
* @param conditionId - Market condition ID
|
|
53
|
+
* @returns Resolved tokens with IDs and outcome names, or null if not found
|
|
54
|
+
*/
|
|
55
|
+
async function resolveMarketTokens(conditionId: string): Promise<ResolvedTokens | null> {
|
|
56
|
+
try {
|
|
57
|
+
const marketService = getMarketService();
|
|
58
|
+
const resolved = await marketService.resolveMarketTokens(conditionId);
|
|
59
|
+
|
|
60
|
+
if (!resolved) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Convert to CTFClient's TokenIds format (legacy naming)
|
|
65
|
+
return {
|
|
66
|
+
tokenIds: {
|
|
67
|
+
yesTokenId: resolved.primaryTokenId,
|
|
68
|
+
noTokenId: resolved.secondaryTokenId,
|
|
69
|
+
},
|
|
70
|
+
primaryOutcome: resolved.primaryOutcome,
|
|
71
|
+
secondaryOutcome: resolved.secondaryOutcome,
|
|
72
|
+
};
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ===== Tool Definitions =====
|
|
79
|
+
|
|
80
|
+
export const onchainToolDefinitions: ToolDefinition[] = [
|
|
81
|
+
{
|
|
82
|
+
name: 'ctf_split',
|
|
83
|
+
description: 'Split USDC into YES + NO tokens for a market. Requires USDC.e (bridged USDC) balance.',
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
conditionId: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
description: 'Market condition ID (0x...)',
|
|
90
|
+
},
|
|
91
|
+
amount: {
|
|
92
|
+
type: 'string',
|
|
93
|
+
description: 'Amount of USDC to split (e.g., "100" for 100 USDC)',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
required: ['conditionId', 'amount'],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'ctf_merge',
|
|
101
|
+
description: `Merge primary + secondary tokens back to USDC.
|
|
102
|
+
|
|
103
|
+
TokenIds are auto-discovered from CLOB API if not provided.
|
|
104
|
+
Works with any outcome types (Yes/No, Up/Down, Team A/B, etc.).`,
|
|
105
|
+
inputSchema: {
|
|
106
|
+
type: 'object',
|
|
107
|
+
properties: {
|
|
108
|
+
conditionId: {
|
|
109
|
+
type: 'string',
|
|
110
|
+
description: 'Market condition ID (0x...)',
|
|
111
|
+
},
|
|
112
|
+
amount: {
|
|
113
|
+
type: 'string',
|
|
114
|
+
description: 'Number of token pairs to merge (e.g., "100" for 100 pairs)',
|
|
115
|
+
},
|
|
116
|
+
tokenIds: {
|
|
117
|
+
type: 'object',
|
|
118
|
+
description: 'Optional: Explicit token IDs {yesTokenId: string, noTokenId: string}. Auto-discovered if not provided.',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
required: ['conditionId', 'amount'],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'ctf_redeem',
|
|
126
|
+
description: `Redeem winning tokens after market resolution.
|
|
127
|
+
|
|
128
|
+
TokenIds are auto-discovered from CLOB API if not provided.
|
|
129
|
+
Outcome names are dynamic (Yes/No, Up/Down, Team A/B, etc.).`,
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {
|
|
133
|
+
conditionId: {
|
|
134
|
+
type: 'string',
|
|
135
|
+
description: 'Market condition ID (0x...)',
|
|
136
|
+
},
|
|
137
|
+
tokenIds: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
description: 'Optional: Explicit token IDs {yesTokenId: string, noTokenId: string}. Auto-discovered if not provided.',
|
|
140
|
+
},
|
|
141
|
+
outcome: {
|
|
142
|
+
type: 'string',
|
|
143
|
+
description: 'Optional: Specific outcome to redeem (e.g., "Yes", "Up", "Team A"). Uses market\'s actual outcome names.',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
required: ['conditionId'],
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: 'get_position_balance',
|
|
151
|
+
description: `Get token balances for a market position.
|
|
152
|
+
|
|
153
|
+
TokenIds are auto-discovered from CLOB API if not provided.
|
|
154
|
+
Returns balances for both primary and secondary outcome tokens.`,
|
|
155
|
+
inputSchema: {
|
|
156
|
+
type: 'object',
|
|
157
|
+
properties: {
|
|
158
|
+
conditionId: {
|
|
159
|
+
type: 'string',
|
|
160
|
+
description: 'Market condition ID (0x...)',
|
|
161
|
+
},
|
|
162
|
+
tokenIds: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
description: 'Optional: Explicit token IDs {yesTokenId: string, noTokenId: string}. Auto-discovered if not provided.',
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
required: ['conditionId'],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: 'get_market_resolution',
|
|
172
|
+
description: 'Check if a market is resolved and get payout information.',
|
|
173
|
+
inputSchema: {
|
|
174
|
+
type: 'object',
|
|
175
|
+
properties: {
|
|
176
|
+
conditionId: {
|
|
177
|
+
type: 'string',
|
|
178
|
+
description: 'Market condition ID (0x...)',
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
required: ['conditionId'],
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: 'check_ctf_ready',
|
|
186
|
+
description: 'Check if wallet is ready for CTF trading (balances, approvals, gas).',
|
|
187
|
+
inputSchema: {
|
|
188
|
+
type: 'object',
|
|
189
|
+
properties: {
|
|
190
|
+
amount: {
|
|
191
|
+
type: 'string',
|
|
192
|
+
description: 'Amount of USDC to check readiness for (e.g., "100")',
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
required: ['amount'],
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'estimate_gas',
|
|
200
|
+
description: 'Estimate gas cost for a CTF operation (split or merge).',
|
|
201
|
+
inputSchema: {
|
|
202
|
+
type: 'object',
|
|
203
|
+
properties: {
|
|
204
|
+
operation: {
|
|
205
|
+
type: 'string',
|
|
206
|
+
enum: ['split', 'merge'],
|
|
207
|
+
description: 'Type of operation to estimate',
|
|
208
|
+
},
|
|
209
|
+
conditionId: {
|
|
210
|
+
type: 'string',
|
|
211
|
+
description: 'Market condition ID (0x...)',
|
|
212
|
+
},
|
|
213
|
+
amount: {
|
|
214
|
+
type: 'string',
|
|
215
|
+
description: 'Amount for the operation',
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
required: ['operation', 'conditionId', 'amount'],
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'get_gas_price',
|
|
223
|
+
description: 'Get current gas price on Polygon network.',
|
|
224
|
+
inputSchema: {
|
|
225
|
+
type: 'object',
|
|
226
|
+
properties: {},
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
// ===== Input Types =====
|
|
232
|
+
|
|
233
|
+
interface CtfSplitInput {
|
|
234
|
+
conditionId: string;
|
|
235
|
+
amount: string;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
interface CtfMergeInput {
|
|
239
|
+
conditionId: string;
|
|
240
|
+
amount: string;
|
|
241
|
+
tokenIds?: TokenIds;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
interface CtfRedeemInput {
|
|
245
|
+
conditionId: string;
|
|
246
|
+
tokenIds?: TokenIds;
|
|
247
|
+
/** Dynamic outcome name - can be Yes/No, Up/Down, Team A/B, etc. */
|
|
248
|
+
outcome?: string;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Convert user outcome to SDK format (uppercase)
|
|
253
|
+
*
|
|
254
|
+
* The SDK expects uppercase outcome names.
|
|
255
|
+
* This function normalizes any outcome name to uppercase.
|
|
256
|
+
*/
|
|
257
|
+
function toSdkOutcome(outcome?: string): string | undefined {
|
|
258
|
+
if (!outcome) return undefined;
|
|
259
|
+
return outcome.toUpperCase();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
interface GetPositionBalanceInput {
|
|
263
|
+
conditionId: string;
|
|
264
|
+
tokenIds?: TokenIds;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
interface GetMarketResolutionInput {
|
|
268
|
+
conditionId: string;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
interface CheckCtfReadyInput {
|
|
272
|
+
amount: string;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
interface EstimateGasInput {
|
|
276
|
+
operation: 'split' | 'merge';
|
|
277
|
+
conditionId: string;
|
|
278
|
+
amount: string;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ===== Helper: Get OnchainService =====
|
|
282
|
+
|
|
283
|
+
// Cache OnchainService instances by wallet address to avoid re-creating
|
|
284
|
+
const onchainServiceCache = new Map<string, OnchainService>();
|
|
285
|
+
|
|
286
|
+
function getOnchainService(): OnchainService {
|
|
287
|
+
const walletManager = getWalletManager();
|
|
288
|
+
const activeWallet = walletManager.getActiveWallet();
|
|
289
|
+
|
|
290
|
+
if (!activeWallet) {
|
|
291
|
+
throw new McpToolError(
|
|
292
|
+
ErrorCode.AUTH_REQUIRED,
|
|
293
|
+
'No wallet configured. Set POLY_PRIVATE_KEY or POLY_WALLETS environment variable.'
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Check cache first
|
|
298
|
+
const cacheKey = activeWallet.address.toLowerCase();
|
|
299
|
+
const cached = onchainServiceCache.get(cacheKey);
|
|
300
|
+
if (cached) {
|
|
301
|
+
return cached;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Create new service for active wallet
|
|
305
|
+
const service = new OnchainService({
|
|
306
|
+
privateKey: activeWallet.wallet.privateKey,
|
|
307
|
+
rpcUrl: process.env.POLY_RPC_URL || 'https://polygon-rpc.com',
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
onchainServiceCache.set(cacheKey, service);
|
|
311
|
+
return service;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ===== Handler Functions =====
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Split USDC into YES + NO tokens
|
|
318
|
+
*/
|
|
319
|
+
export async function handleCtfSplit(input: CtfSplitInput) {
|
|
320
|
+
validateConditionId(input.conditionId);
|
|
321
|
+
|
|
322
|
+
if (!input.amount || parseFloat(input.amount) <= 0) {
|
|
323
|
+
throw new McpToolError(
|
|
324
|
+
ErrorCode.INVALID_INPUT,
|
|
325
|
+
'Amount must be a positive number'
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const service = getOnchainService();
|
|
331
|
+
|
|
332
|
+
// Check if can split
|
|
333
|
+
const canSplitCheck = await service.canSplit(input.amount);
|
|
334
|
+
if (!canSplitCheck.canSplit) {
|
|
335
|
+
return {
|
|
336
|
+
success: false,
|
|
337
|
+
error: canSplitCheck.reason,
|
|
338
|
+
wallet: service.getAddress(),
|
|
339
|
+
conditionId: input.conditionId,
|
|
340
|
+
amount: input.amount,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Execute split
|
|
345
|
+
const result = await service.split(input.conditionId, input.amount);
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
success: result.success,
|
|
349
|
+
wallet: service.getAddress(),
|
|
350
|
+
conditionId: input.conditionId,
|
|
351
|
+
amount: input.amount,
|
|
352
|
+
yesTokens: result.yesTokens,
|
|
353
|
+
noTokens: result.noTokens,
|
|
354
|
+
txHash: result.txHash,
|
|
355
|
+
gasUsed: result.gasUsed,
|
|
356
|
+
explorerUrl: `https://polygonscan.com/tx/${result.txHash}`,
|
|
357
|
+
message: `Split ${input.amount} USDC into ${result.yesTokens} YES + ${result.noTokens} NO tokens`,
|
|
358
|
+
};
|
|
359
|
+
} catch (err) {
|
|
360
|
+
throw wrapError(err);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Merge primary + secondary tokens back to USDC
|
|
366
|
+
*
|
|
367
|
+
* Automatically discovers tokenIds from CLOB API if not provided.
|
|
368
|
+
*/
|
|
369
|
+
export async function handleCtfMerge(input: CtfMergeInput) {
|
|
370
|
+
validateConditionId(input.conditionId);
|
|
371
|
+
|
|
372
|
+
if (!input.amount || parseFloat(input.amount) <= 0) {
|
|
373
|
+
throw new McpToolError(
|
|
374
|
+
ErrorCode.INVALID_INPUT,
|
|
375
|
+
'Amount must be a positive number'
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
const service = getOnchainService();
|
|
381
|
+
|
|
382
|
+
// Auto-discover tokenIds if not provided
|
|
383
|
+
let tokenIds = input.tokenIds;
|
|
384
|
+
let resolvedTokens: ResolvedTokens | null = null;
|
|
385
|
+
let tokenIdsSource: 'provided' | 'auto-discovered' | 'calculated' = 'provided';
|
|
386
|
+
|
|
387
|
+
if (!tokenIds) {
|
|
388
|
+
resolvedTokens = await resolveMarketTokens(input.conditionId);
|
|
389
|
+
if (resolvedTokens) {
|
|
390
|
+
tokenIds = resolvedTokens.tokenIds;
|
|
391
|
+
tokenIdsSource = 'auto-discovered';
|
|
392
|
+
} else {
|
|
393
|
+
tokenIdsSource = 'calculated';
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Check if can merge
|
|
398
|
+
let canMergeCheck;
|
|
399
|
+
if (tokenIds) {
|
|
400
|
+
canMergeCheck = await service.canMergeWithTokenIds(
|
|
401
|
+
input.conditionId,
|
|
402
|
+
tokenIds,
|
|
403
|
+
input.amount
|
|
404
|
+
);
|
|
405
|
+
} else {
|
|
406
|
+
canMergeCheck = await service.canMerge(input.conditionId, input.amount);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (!canMergeCheck.canMerge) {
|
|
410
|
+
// Replace hardcoded YES/NO in error message with dynamic outcome names
|
|
411
|
+
let errorMessage = canMergeCheck.reason || 'Insufficient tokens';
|
|
412
|
+
if (resolvedTokens) {
|
|
413
|
+
errorMessage = errorMessage
|
|
414
|
+
.replace(/YES tokens/g, `${resolvedTokens.primaryOutcome} tokens`)
|
|
415
|
+
.replace(/NO tokens/g, `${resolvedTokens.secondaryOutcome} tokens`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
success: false,
|
|
420
|
+
error: errorMessage,
|
|
421
|
+
wallet: service.getAddress(),
|
|
422
|
+
conditionId: input.conditionId,
|
|
423
|
+
amount: input.amount,
|
|
424
|
+
tokenIdsSource,
|
|
425
|
+
outcomeNames: resolvedTokens
|
|
426
|
+
? { primary: resolvedTokens.primaryOutcome, secondary: resolvedTokens.secondaryOutcome }
|
|
427
|
+
: undefined,
|
|
428
|
+
hint: tokenIdsSource === 'calculated'
|
|
429
|
+
? 'TokenIds could not be auto-discovered. This market may require explicit tokenIds.'
|
|
430
|
+
: undefined,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Execute merge
|
|
435
|
+
let result;
|
|
436
|
+
if (tokenIds) {
|
|
437
|
+
result = await service.mergeByTokenIds(
|
|
438
|
+
input.conditionId,
|
|
439
|
+
tokenIds,
|
|
440
|
+
input.amount
|
|
441
|
+
);
|
|
442
|
+
} else {
|
|
443
|
+
result = await service.merge(input.conditionId, input.amount);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Use dynamic outcome names if available
|
|
447
|
+
const primaryName = resolvedTokens?.primaryOutcome ?? 'YES';
|
|
448
|
+
const secondaryName = resolvedTokens?.secondaryOutcome ?? 'NO';
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
success: result.success,
|
|
452
|
+
wallet: service.getAddress(),
|
|
453
|
+
conditionId: input.conditionId,
|
|
454
|
+
amount: input.amount,
|
|
455
|
+
usdcReceived: result.usdcReceived,
|
|
456
|
+
txHash: result.txHash,
|
|
457
|
+
gasUsed: result.gasUsed,
|
|
458
|
+
explorerUrl: `https://polygonscan.com/tx/${result.txHash}`,
|
|
459
|
+
tokenIdsSource,
|
|
460
|
+
outcomeNames: resolvedTokens
|
|
461
|
+
? { primary: resolvedTokens.primaryOutcome, secondary: resolvedTokens.secondaryOutcome }
|
|
462
|
+
: undefined,
|
|
463
|
+
message: `Merged ${input.amount} ${primaryName} + ${input.amount} ${secondaryName} tokens into ${result.usdcReceived} USDC`,
|
|
464
|
+
};
|
|
465
|
+
} catch (err) {
|
|
466
|
+
throw wrapError(err);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Redeem winning tokens after market resolution
|
|
472
|
+
*
|
|
473
|
+
* Automatically discovers tokenIds from CLOB API if not provided.
|
|
474
|
+
*/
|
|
475
|
+
export async function handleCtfRedeem(input: CtfRedeemInput) {
|
|
476
|
+
validateConditionId(input.conditionId);
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
const service = getOnchainService();
|
|
480
|
+
|
|
481
|
+
// Check market resolution first
|
|
482
|
+
const resolution = await service.getMarketResolution(input.conditionId);
|
|
483
|
+
if (!resolution.isResolved) {
|
|
484
|
+
return {
|
|
485
|
+
success: false,
|
|
486
|
+
error: 'Market is not yet resolved. Cannot redeem tokens.',
|
|
487
|
+
wallet: service.getAddress(),
|
|
488
|
+
conditionId: input.conditionId,
|
|
489
|
+
isResolved: false,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Auto-discover tokenIds if not provided
|
|
494
|
+
let tokenIds = input.tokenIds;
|
|
495
|
+
let resolvedTokens: ResolvedTokens | null = null;
|
|
496
|
+
let tokenIdsSource: 'provided' | 'auto-discovered' | 'calculated' = 'provided';
|
|
497
|
+
|
|
498
|
+
if (!tokenIds) {
|
|
499
|
+
resolvedTokens = await resolveMarketTokens(input.conditionId);
|
|
500
|
+
if (resolvedTokens) {
|
|
501
|
+
tokenIds = resolvedTokens.tokenIds;
|
|
502
|
+
tokenIdsSource = 'auto-discovered';
|
|
503
|
+
} else {
|
|
504
|
+
tokenIdsSource = 'calculated';
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Execute redeem - convert outcome format for SDK
|
|
509
|
+
const sdkOutcome = toSdkOutcome(input.outcome);
|
|
510
|
+
let result;
|
|
511
|
+
if (tokenIds) {
|
|
512
|
+
result = await service.redeemByTokenIds(
|
|
513
|
+
input.conditionId,
|
|
514
|
+
tokenIds,
|
|
515
|
+
sdkOutcome
|
|
516
|
+
);
|
|
517
|
+
} else {
|
|
518
|
+
// Fallback to calculated positionIds (may fail for CLOB markets)
|
|
519
|
+
result = await service.redeem(input.conditionId, sdkOutcome);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Use dynamic outcome names if available
|
|
523
|
+
const primaryName = resolvedTokens?.primaryOutcome ?? 'YES';
|
|
524
|
+
const secondaryName = resolvedTokens?.secondaryOutcome ?? 'NO';
|
|
525
|
+
const winningOutcomeName = resolution.winningOutcome === 'YES'
|
|
526
|
+
? primaryName
|
|
527
|
+
: resolution.winningOutcome === 'NO'
|
|
528
|
+
? secondaryName
|
|
529
|
+
: resolution.winningOutcome;
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
success: result.success,
|
|
533
|
+
wallet: service.getAddress(),
|
|
534
|
+
conditionId: input.conditionId,
|
|
535
|
+
winningOutcome: winningOutcomeName,
|
|
536
|
+
tokensRedeemed: result.tokensRedeemed,
|
|
537
|
+
usdcReceived: result.usdcReceived,
|
|
538
|
+
txHash: result.txHash,
|
|
539
|
+
gasUsed: result.gasUsed,
|
|
540
|
+
explorerUrl: `https://polygonscan.com/tx/${result.txHash}`,
|
|
541
|
+
tokenIdsSource,
|
|
542
|
+
outcomeNames: resolvedTokens
|
|
543
|
+
? { primary: resolvedTokens.primaryOutcome, secondary: resolvedTokens.secondaryOutcome }
|
|
544
|
+
: undefined,
|
|
545
|
+
message: `Redeemed ${result.tokensRedeemed} winning ${winningOutcomeName} tokens for ${result.usdcReceived} USDC`,
|
|
546
|
+
};
|
|
547
|
+
} catch (err) {
|
|
548
|
+
throw wrapError(err);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Get token balances for a market position
|
|
554
|
+
*
|
|
555
|
+
* Automatically discovers tokenIds from CLOB API if not provided.
|
|
556
|
+
*/
|
|
557
|
+
export async function handleGetPositionBalance(input: GetPositionBalanceInput) {
|
|
558
|
+
validateConditionId(input.conditionId);
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
const service = getOnchainService();
|
|
562
|
+
|
|
563
|
+
// Auto-discover tokenIds if not provided
|
|
564
|
+
let tokenIds = input.tokenIds;
|
|
565
|
+
let resolvedTokens: ResolvedTokens | null = null;
|
|
566
|
+
let tokenIdsSource: 'provided' | 'auto-discovered' | 'calculated' = 'provided';
|
|
567
|
+
|
|
568
|
+
if (!tokenIds) {
|
|
569
|
+
resolvedTokens = await resolveMarketTokens(input.conditionId);
|
|
570
|
+
if (resolvedTokens) {
|
|
571
|
+
tokenIds = resolvedTokens.tokenIds;
|
|
572
|
+
tokenIdsSource = 'auto-discovered';
|
|
573
|
+
} else {
|
|
574
|
+
tokenIdsSource = 'calculated';
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
let balance;
|
|
579
|
+
if (tokenIds) {
|
|
580
|
+
balance = await service.getPositionBalanceByTokenIds(
|
|
581
|
+
input.conditionId,
|
|
582
|
+
tokenIds
|
|
583
|
+
);
|
|
584
|
+
} else {
|
|
585
|
+
balance = await service.getPositionBalance(input.conditionId);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Use dynamic outcome names if available
|
|
589
|
+
const primaryName = resolvedTokens?.primaryOutcome ?? 'yes';
|
|
590
|
+
const secondaryName = resolvedTokens?.secondaryOutcome ?? 'no';
|
|
591
|
+
|
|
592
|
+
return {
|
|
593
|
+
wallet: service.getAddress(),
|
|
594
|
+
conditionId: input.conditionId,
|
|
595
|
+
balances: {
|
|
596
|
+
[primaryName.toLowerCase()]: balance.yesBalance,
|
|
597
|
+
[secondaryName.toLowerCase()]: balance.noBalance,
|
|
598
|
+
},
|
|
599
|
+
positionIds: {
|
|
600
|
+
[primaryName.toLowerCase()]: balance.yesPositionId,
|
|
601
|
+
[secondaryName.toLowerCase()]: balance.noPositionId,
|
|
602
|
+
},
|
|
603
|
+
outcomeNames: resolvedTokens
|
|
604
|
+
? { primary: resolvedTokens.primaryOutcome, secondary: resolvedTokens.secondaryOutcome }
|
|
605
|
+
: undefined,
|
|
606
|
+
tokenIdsSource,
|
|
607
|
+
hasPosition: parseFloat(balance.yesBalance) > 0 || parseFloat(balance.noBalance) > 0,
|
|
608
|
+
};
|
|
609
|
+
} catch (err) {
|
|
610
|
+
throw wrapError(err);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Check if market is resolved
|
|
616
|
+
*/
|
|
617
|
+
export async function handleGetMarketResolution(input: GetMarketResolutionInput) {
|
|
618
|
+
validateConditionId(input.conditionId);
|
|
619
|
+
|
|
620
|
+
try {
|
|
621
|
+
const service = getOnchainService();
|
|
622
|
+
const resolution = await service.getMarketResolution(input.conditionId);
|
|
623
|
+
|
|
624
|
+
return {
|
|
625
|
+
conditionId: input.conditionId,
|
|
626
|
+
isResolved: resolution.isResolved,
|
|
627
|
+
winningOutcome: resolution.winningOutcome,
|
|
628
|
+
payoutNumerators: resolution.payoutNumerators,
|
|
629
|
+
payoutDenominator: resolution.payoutDenominator,
|
|
630
|
+
message: resolution.isResolved
|
|
631
|
+
? `Market resolved: ${resolution.winningOutcome} wins`
|
|
632
|
+
: 'Market is not yet resolved',
|
|
633
|
+
};
|
|
634
|
+
} catch (err) {
|
|
635
|
+
throw wrapError(err);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Check if wallet is ready for CTF trading
|
|
641
|
+
*/
|
|
642
|
+
export async function handleCheckCtfReady(input: CheckCtfReadyInput) {
|
|
643
|
+
if (!input.amount || parseFloat(input.amount) <= 0) {
|
|
644
|
+
throw new McpToolError(
|
|
645
|
+
ErrorCode.INVALID_INPUT,
|
|
646
|
+
'Amount must be a positive number'
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
try {
|
|
651
|
+
const service = getOnchainService();
|
|
652
|
+
const status = await service.checkReadyForCTF(input.amount);
|
|
653
|
+
|
|
654
|
+
return {
|
|
655
|
+
wallet: service.getAddress(),
|
|
656
|
+
ready: status.ready,
|
|
657
|
+
balances: {
|
|
658
|
+
usdcE: status.usdcEBalance,
|
|
659
|
+
nativeUsdc: status.nativeUsdcBalance,
|
|
660
|
+
matic: status.maticBalance,
|
|
661
|
+
},
|
|
662
|
+
tradingReady: status.tradingReady,
|
|
663
|
+
issues: status.issues,
|
|
664
|
+
suggestion: status.suggestion,
|
|
665
|
+
message: status.ready
|
|
666
|
+
? `Ready to trade with ${input.amount} USDC`
|
|
667
|
+
: `Not ready: ${status.issues.join(', ')}`,
|
|
668
|
+
};
|
|
669
|
+
} catch (err) {
|
|
670
|
+
throw wrapError(err);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Estimate gas for CTF operation
|
|
676
|
+
*/
|
|
677
|
+
export async function handleEstimateGas(input: EstimateGasInput) {
|
|
678
|
+
validateConditionId(input.conditionId);
|
|
679
|
+
|
|
680
|
+
if (!input.amount || parseFloat(input.amount) <= 0) {
|
|
681
|
+
throw new McpToolError(
|
|
682
|
+
ErrorCode.INVALID_INPUT,
|
|
683
|
+
'Amount must be a positive number'
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (!['split', 'merge'].includes(input.operation)) {
|
|
688
|
+
throw new McpToolError(
|
|
689
|
+
ErrorCode.INVALID_INPUT,
|
|
690
|
+
'Operation must be "split" or "merge"'
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
try {
|
|
695
|
+
const service = getOnchainService();
|
|
696
|
+
|
|
697
|
+
let estimate;
|
|
698
|
+
if (input.operation === 'split') {
|
|
699
|
+
estimate = await service.estimateSplitGas(input.conditionId, input.amount);
|
|
700
|
+
} else {
|
|
701
|
+
estimate = await service.estimateMergeGas(input.conditionId, input.amount);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return {
|
|
705
|
+
operation: input.operation,
|
|
706
|
+
conditionId: input.conditionId,
|
|
707
|
+
amount: input.amount,
|
|
708
|
+
gas: {
|
|
709
|
+
gasUnits: estimate.gasUnits,
|
|
710
|
+
gasPriceGwei: estimate.gasPriceGwei,
|
|
711
|
+
costMatic: estimate.costMatic,
|
|
712
|
+
costUsdc: estimate.costUsdc,
|
|
713
|
+
maticPrice: estimate.maticPrice,
|
|
714
|
+
},
|
|
715
|
+
message: `Estimated gas for ${input.operation}: ${estimate.costMatic} MATIC (~$${estimate.costUsdc})`,
|
|
716
|
+
};
|
|
717
|
+
} catch (err) {
|
|
718
|
+
throw wrapError(err);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Get current gas price
|
|
724
|
+
*/
|
|
725
|
+
export async function handleGetGasPrice() {
|
|
726
|
+
try {
|
|
727
|
+
const service = getOnchainService();
|
|
728
|
+
const gasPrice = await service.getGasPrice();
|
|
729
|
+
|
|
730
|
+
return {
|
|
731
|
+
gwei: gasPrice.gwei,
|
|
732
|
+
wei: gasPrice.wei,
|
|
733
|
+
message: `Current gas price: ${gasPrice.gwei} Gwei`,
|
|
734
|
+
};
|
|
735
|
+
} catch (err) {
|
|
736
|
+
throw wrapError(err);
|
|
737
|
+
}
|
|
738
|
+
}
|