polycopy 0.0.1

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/dist/index.js ADDED
@@ -0,0 +1,1048 @@
1
+ "use strict";
2
+ const init = require("./init-C8vHJzZn.js");
3
+ const grammy = require("grammy");
4
+ const wallet = require("@ethersproject/wallet");
5
+ const clobClient = require("@polymarket/clob-client");
6
+ const builderSigningSdk = require("@polymarket/builder-signing-sdk");
7
+ const viem = require("viem");
8
+ const accounts = require("viem/accounts");
9
+ const chains = require("viem/chains");
10
+ const builderRelayerClient = require("@polymarket/builder-relayer-client");
11
+ const setPromiseInterval = require("set-promise-interval");
12
+ const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
13
+ const setPromiseInterval__default = /* @__PURE__ */ _interopDefault(setPromiseInterval);
14
+ class Notifier {
15
+ bot = null;
16
+ adminChatId = null;
17
+ initialized = false;
18
+ /**
19
+ * 初始化通知器
20
+ */
21
+ init(botToken, adminChatId) {
22
+ if (!adminChatId) {
23
+ init.logger.warning("未配置管理员 Chat ID,通知功能已禁用");
24
+ return;
25
+ }
26
+ this.bot = new grammy.Bot(botToken);
27
+ this.adminChatId = adminChatId;
28
+ this.initialized = true;
29
+ init.logger.info("管理员通知已启用");
30
+ }
31
+ /**
32
+ * 发送错误通知
33
+ */
34
+ async error(message, details) {
35
+ await this.send(`🔴 ${message}`, details);
36
+ }
37
+ /**
38
+ * 发送警告通知
39
+ */
40
+ async warning(message, details) {
41
+ await this.send(`🟡 ${message}`, details);
42
+ }
43
+ /**
44
+ * 发送成功通知
45
+ */
46
+ async success(message, details) {
47
+ await this.send(`🟢 ${message}`, details);
48
+ }
49
+ /**
50
+ * 发送消息给管理员
51
+ */
52
+ async send(message, details) {
53
+ if (!this.initialized || !this.bot || !this.adminChatId) {
54
+ return;
55
+ }
56
+ const fullMessage = details ? `${message}
57
+
58
+ ${details}` : message;
59
+ try {
60
+ await this.bot.api.sendMessage(this.adminChatId, fullMessage, {
61
+ parse_mode: "HTML"
62
+ });
63
+ } catch (error) {
64
+ const errorMsg = error instanceof Error ? error.message : String(error);
65
+ init.logger.error(`发送通知失败: ${errorMsg}`);
66
+ }
67
+ }
68
+ }
69
+ const notifier = new Notifier();
70
+ const CLOB_HOST = "https://clob.polymarket.com";
71
+ class ClobClientWrapper {
72
+ client = null;
73
+ initialized = false;
74
+ /**
75
+ * 初始化客户端
76
+ */
77
+ async init(config) {
78
+ if (this.initialized) {
79
+ return;
80
+ }
81
+ const wallet$1 = new wallet.Wallet(config.privateKey);
82
+ init.logger.info(`钱包地址: ${wallet$1.address}`);
83
+ const tempClient = new clobClient.ClobClient(CLOB_HOST, clobClient.Chain.POLYGON, wallet$1);
84
+ this.patchClient(tempClient);
85
+ const creds = await tempClient.deriveApiKey();
86
+ init.logger.success("API 凭证已获取");
87
+ let builderConfig;
88
+ if (config.builderCreds) {
89
+ builderConfig = new builderSigningSdk.BuilderConfig({
90
+ localBuilderCreds: config.builderCreds
91
+ });
92
+ }
93
+ this.client = new clobClient.ClobClient(
94
+ CLOB_HOST,
95
+ clobClient.Chain.POLYGON,
96
+ wallet$1,
97
+ creds,
98
+ 2,
99
+ // POLY_GNOSIS_SAFE
100
+ config.funderAddress,
101
+ // Polymarket 账户地址(页面右上角)
102
+ void 0,
103
+ // geoBlockToken
104
+ true,
105
+ // useServerTime
106
+ builderConfig
107
+ );
108
+ this.patchClient(this.client);
109
+ this.initialized = true;
110
+ init.logger.success("ClobClient 初始化完成");
111
+ }
112
+ /**
113
+ * 魔法操作:clob-client 请求出错不抛异常,只返回 { error: ... }
114
+ * 参考:https://github.com/Polymarket/clob-client/blob/96e25998b8ae78be27855e07faf15099602bcd47/src/http-helpers/index.ts#L90
115
+ * 这里统一包装,让它抛出异常
116
+ */
117
+ patchClient(client) {
118
+ const c = client;
119
+ for (const method of ["get", "post", "del"]) {
120
+ const original = c[method];
121
+ c[method] = function(...args) {
122
+ return original.call(c, ...args).then((result) => {
123
+ if (result?.error) {
124
+ throw new Error(
125
+ typeof result.error === "string" ? result.error : JSON.stringify(result.error)
126
+ );
127
+ }
128
+ return result;
129
+ });
130
+ };
131
+ }
132
+ }
133
+ /**
134
+ * 确保客户端已初始化
135
+ */
136
+ ensureInitialized() {
137
+ if (!this.client || !this.initialized) {
138
+ throw new Error("ClobClient 未初始化");
139
+ }
140
+ return this.client;
141
+ }
142
+ /**
143
+ * 获取 USDC 余额(已处理 6 位小数精度)
144
+ */
145
+ async getBalance() {
146
+ const client = this.ensureInitialized();
147
+ const result = await client.getBalanceAllowance({
148
+ asset_type: clobClient.AssetType.COLLATERAL
149
+ });
150
+ return parseFloat(result.balance) / 1e6;
151
+ }
152
+ /**
153
+ * 获取 token 的 tickSize
154
+ */
155
+ async getTickSize(tokenId) {
156
+ const client = this.ensureInitialized();
157
+ return await client.getTickSize(tokenId);
158
+ }
159
+ /**
160
+ * 获取 token 是否为 negRisk
161
+ */
162
+ async getNegRisk(tokenId) {
163
+ const client = this.ensureInitialized();
164
+ return await client.getNegRisk(tokenId);
165
+ }
166
+ /**
167
+ * 创建买单(GTC 限价单)
168
+ */
169
+ async createBuyOrder(tokenId, price, size) {
170
+ const client = this.ensureInitialized();
171
+ const userOrder = {
172
+ tokenID: tokenId,
173
+ price,
174
+ size,
175
+ side: clobClient.Side.BUY
176
+ };
177
+ try {
178
+ const tickSize = await this.getTickSize(tokenId);
179
+ const negRisk = await this.getNegRisk(tokenId);
180
+ const response = await client.createAndPostOrder(
181
+ userOrder,
182
+ { tickSize, negRisk },
183
+ clobClient.OrderType.GTC
184
+ );
185
+ if (response.success) {
186
+ init.logger.success(`买单创建成功: ${response.orderID}`);
187
+ return {
188
+ success: true,
189
+ orderId: response.orderID
190
+ };
191
+ } else {
192
+ init.logger.error(`买单创建失败: ${response.errorMsg}`);
193
+ return {
194
+ success: false,
195
+ orderId: "",
196
+ errorMsg: response.errorMsg
197
+ };
198
+ }
199
+ } catch (error) {
200
+ const errorMsg = error instanceof Error ? error.message : String(error);
201
+ init.logger.error(`买单创建异常: ${errorMsg}`);
202
+ return {
203
+ success: false,
204
+ orderId: "",
205
+ errorMsg
206
+ };
207
+ }
208
+ }
209
+ /**
210
+ * 创建卖单(GTC 限价单)
211
+ */
212
+ async createSellOrder(tokenId, price, size) {
213
+ const client = this.ensureInitialized();
214
+ const userOrder = {
215
+ tokenID: tokenId,
216
+ price,
217
+ size,
218
+ side: clobClient.Side.SELL
219
+ };
220
+ try {
221
+ const tickSize = await this.getTickSize(tokenId);
222
+ const negRisk = await this.getNegRisk(tokenId);
223
+ const response = await client.createAndPostOrder(
224
+ userOrder,
225
+ { tickSize, negRisk },
226
+ clobClient.OrderType.GTC
227
+ );
228
+ if (response.success) {
229
+ return {
230
+ success: true,
231
+ orderId: response.orderID
232
+ };
233
+ } else {
234
+ return {
235
+ success: false,
236
+ orderId: "",
237
+ errorMsg: response.errorMsg
238
+ };
239
+ }
240
+ } catch (error) {
241
+ const errorMsg = error instanceof Error ? error.message : String(error);
242
+ return {
243
+ success: false,
244
+ orderId: "",
245
+ errorMsg
246
+ };
247
+ }
248
+ }
249
+ /**
250
+ * 获取订单状态
251
+ */
252
+ async getOrder(orderId) {
253
+ const client = this.ensureInitialized();
254
+ return await client.getOrder(orderId);
255
+ }
256
+ /**
257
+ * 取消订单
258
+ */
259
+ async cancelOrder(orderId) {
260
+ const client = this.ensureInitialized();
261
+ try {
262
+ await client.cancelOrder({ orderID: orderId });
263
+ init.logger.info(`订单已取消: ${orderId}`);
264
+ return true;
265
+ } catch (error) {
266
+ const errorMsg = error instanceof Error ? error.message : String(error);
267
+ init.logger.error(`取消订单失败: ${errorMsg}`);
268
+ return false;
269
+ }
270
+ }
271
+ /**
272
+ * 获取指定 token 的持仓数量
273
+ */
274
+ async getPositionSize(tokenId) {
275
+ const client = this.ensureInitialized();
276
+ const result = await client.getBalanceAllowance({
277
+ asset_type: clobClient.AssetType.CONDITIONAL,
278
+ token_id: tokenId
279
+ });
280
+ return parseFloat(result.balance) / 1e6;
281
+ }
282
+ }
283
+ const RPC_URL = "https://polygon-rpc.com";
284
+ const CTF_ADDRESS = viem.getAddress("0x4d97dcd97ec945f40cf65f87097ace5ea0476045");
285
+ const USDC_ADDRESS = viem.getAddress("0x2791bca1f2de4661ed88a30c99a7a9449aa84174");
286
+ class RelayClientWrapper {
287
+ relayClient = null;
288
+ initialized = false;
289
+ /**
290
+ * 初始化 RelayClient
291
+ */
292
+ init(config) {
293
+ if (this.initialized) {
294
+ return;
295
+ }
296
+ const account = accounts.privateKeyToAccount(config.privateKey);
297
+ const walletClient = viem.createWalletClient({
298
+ account,
299
+ chain: chains.polygon,
300
+ transport: viem.http(RPC_URL)
301
+ });
302
+ const builderConfig = new builderSigningSdk.BuilderConfig({
303
+ localBuilderCreds: config.builderCreds
304
+ });
305
+ this.relayClient = new builderRelayerClient.RelayClient(
306
+ "https://relayer-v2.polymarket.com",
307
+ 137,
308
+ walletClient,
309
+ builderConfig,
310
+ builderRelayerClient.RelayerTxType.SAFE
311
+ );
312
+ this.initialized = true;
313
+ init.logger.success("RelayClient 初始化完成");
314
+ }
315
+ /**
316
+ * 检查是否已初始化
317
+ */
318
+ isInitialized() {
319
+ return this.initialized;
320
+ }
321
+ /**
322
+ * 执行 Redeem
323
+ * @param conditionId 条件 ID
324
+ * @returns 交易哈希
325
+ */
326
+ async redeem(conditionId) {
327
+ if (!this.relayClient || !this.initialized) {
328
+ throw new Error("RelayClient 未初始化");
329
+ }
330
+ const calldata = viem.encodeFunctionData({
331
+ abi: [
332
+ {
333
+ name: "redeemPositions",
334
+ type: "function",
335
+ inputs: [
336
+ { name: "collateralToken", type: "address" },
337
+ { name: "parentCollectionId", type: "bytes32" },
338
+ { name: "conditionId", type: "bytes32" },
339
+ { name: "indexSets", type: "uint256[]" }
340
+ ],
341
+ outputs: [],
342
+ stateMutability: "nonpayable"
343
+ }
344
+ ],
345
+ functionName: "redeemPositions",
346
+ args: [USDC_ADDRESS, viem.zeroHash, conditionId, [1n, 2n]]
347
+ });
348
+ const tx = {
349
+ to: CTF_ADDRESS,
350
+ value: "0",
351
+ data: calldata
352
+ };
353
+ const response = await this.relayClient.execute(
354
+ [tx],
355
+ "Polymarket redeem positions"
356
+ );
357
+ const result = await response.wait();
358
+ if (!result) {
359
+ throw new Error(
360
+ "Redeem 交易失败:wait() 返回空结果(可能被 revert 或 relayer 拒绝)"
361
+ );
362
+ }
363
+ return result.transactionHash;
364
+ }
365
+ }
366
+ class AssetFilter {
367
+ // endTime -> Set<assetId>
368
+ traded = /* @__PURE__ */ new Map();
369
+ currentEndTime = 0;
370
+ /**
371
+ * 检查 assetId 是否可以交易
372
+ */
373
+ canTrade(assetId, endTime) {
374
+ if (endTime !== this.currentEndTime) {
375
+ this.cleanupBefore(endTime);
376
+ this.currentEndTime = endTime;
377
+ }
378
+ const tradedSet = this.traded.get(endTime);
379
+ if (!tradedSet) {
380
+ return true;
381
+ }
382
+ return !tradedSet.has(assetId);
383
+ }
384
+ /**
385
+ * 标记 assetId 已交易
386
+ */
387
+ markTraded(assetId, endTime) {
388
+ if (!this.traded.has(endTime)) {
389
+ this.traded.set(endTime, /* @__PURE__ */ new Set());
390
+ }
391
+ this.traded.get(endTime).add(assetId);
392
+ init.logger.info(`已标记 assetId 交易完成: ${assetId.slice(0, 10)}...`);
393
+ }
394
+ /**
395
+ * 清理过期记录
396
+ */
397
+ cleanupBefore(currentEndTime) {
398
+ let cleanedCount = 0;
399
+ for (const endTime of this.traded.keys()) {
400
+ if (endTime < currentEndTime) {
401
+ this.traded.delete(endTime);
402
+ cleanedCount++;
403
+ }
404
+ }
405
+ if (cleanedCount > 0) {
406
+ init.logger.info(`已清理 ${cleanedCount} 个过期周期的交易记录`);
407
+ }
408
+ }
409
+ }
410
+ class BalanceCache {
411
+ // endTime -> balance
412
+ cache = /* @__PURE__ */ new Map();
413
+ currentEndTime = 0;
414
+ /**
415
+ * 获取余额(带缓存)
416
+ * @param endTime 事件结束时间
417
+ * @param fetchFn 获取余额的函数
418
+ */
419
+ async getBalance(endTime, fetchFn) {
420
+ if (endTime !== this.currentEndTime) {
421
+ this.cleanupBefore(endTime);
422
+ this.currentEndTime = endTime;
423
+ }
424
+ if (this.cache.has(endTime)) {
425
+ const cached = this.cache.get(endTime);
426
+ init.logger.info(`使用缓存余额: ${cached} USDC`);
427
+ return cached;
428
+ }
429
+ const balance = await fetchFn();
430
+ this.cache.set(endTime, balance);
431
+ init.logger.info(`获取当前余额: ${balance} USDC`);
432
+ return balance;
433
+ }
434
+ /**
435
+ * 清除所有缓存(Redeem 成功后调用)
436
+ */
437
+ invalidate() {
438
+ if (this.cache.size > 0) {
439
+ this.cache.clear();
440
+ init.logger.info("余额缓存已清除");
441
+ }
442
+ }
443
+ /**
444
+ * 清理过期缓存
445
+ */
446
+ cleanupBefore(currentEndTime) {
447
+ let cleanedCount = 0;
448
+ for (const endTime of this.cache.keys()) {
449
+ if (endTime < currentEndTime) {
450
+ this.cache.delete(endTime);
451
+ cleanedCount++;
452
+ }
453
+ }
454
+ if (cleanedCount > 0) {
455
+ init.logger.info(`已清理 ${cleanedCount} 个过期周期的余额缓存`);
456
+ }
457
+ }
458
+ }
459
+ const TERMINAL_STATUSES = ["MATCHED", "CONFIRMED", "FAILED", "CANCELLED"];
460
+ const POLL_INTERVAL = 3e3;
461
+ const TIMEOUT_BUFFER = 2 * 60 * 1e3;
462
+ class OrderWatcher {
463
+ /**
464
+ * 监控订单直到进入终态或超时
465
+ * @param orderId 订单 ID
466
+ * @param endTime 事件结束时间(毫秒)
467
+ * @param getOrderFn 获取订单的函数
468
+ */
469
+ async watch(orderId, endTime, getOrderFn) {
470
+ const timeoutAt = endTime + TIMEOUT_BUFFER;
471
+ const shortId = orderId.substring(0, 10) + "...";
472
+ init.logger.info(`开始监控订单 ${shortId},超时: ${new Date(timeoutAt).toLocaleString()}`);
473
+ while (true) {
474
+ const now = Date.now();
475
+ if (now >= timeoutAt) {
476
+ init.logger.warning(`订单超时: ${shortId}`);
477
+ return { status: "TIMEOUT" };
478
+ }
479
+ try {
480
+ const order = await getOrderFn(orderId);
481
+ const status = order.status.toUpperCase();
482
+ if (TERMINAL_STATUSES.includes(status)) {
483
+ const filledSize = parseFloat(order.size_matched || "0");
484
+ init.logger.info(`订单 ${shortId} 终态: ${status},成交: ${filledSize}`);
485
+ return {
486
+ status,
487
+ filledSize,
488
+ order
489
+ };
490
+ }
491
+ } catch (error) {
492
+ const errorMsg = error instanceof Error ? error.message : String(error);
493
+ init.logger.warning(`查询订单失败: ${errorMsg},继续重试...`);
494
+ }
495
+ await this.delay(POLL_INTERVAL);
496
+ }
497
+ }
498
+ delay(ms) {
499
+ return new Promise((resolve) => setTimeout(resolve, ms));
500
+ }
501
+ }
502
+ const DATA_API_HOST = "https://data-api.polymarket.com";
503
+ const BASE_INTERVAL = 5 * 60 * 1e3;
504
+ const MAX_MATCH_TIMES = 3;
505
+ class Redeemer {
506
+ // conditionId -> RedeemRecord
507
+ records = /* @__PURE__ */ new Map();
508
+ intervalId;
509
+ delayTimeoutId;
510
+ running = false;
511
+ redeemFn = null;
512
+ funderAddress = "";
513
+ onRedeemSuccess = null;
514
+ /**
515
+ * 启动 Redeemer
516
+ * @param redeemFn 执行 Redeem 的函数(返回交易哈希)
517
+ * @param funderAddress Polymarket 账户地址(用于查询仓位)
518
+ * @param onRedeemSuccess Redeem 成功后的回调(用于更新余额缓存)
519
+ */
520
+ start(redeemFn, funderAddress, onRedeemSuccess) {
521
+ if (this.running) {
522
+ return;
523
+ }
524
+ this.redeemFn = redeemFn;
525
+ this.funderAddress = funderAddress;
526
+ this.onRedeemSuccess = onRedeemSuccess || null;
527
+ this.running = true;
528
+ init.logger.info("Redeemer 已启动");
529
+ this.intervalId = setPromiseInterval__default.default(async () => {
530
+ await this.runOnce();
531
+ }, BASE_INTERVAL);
532
+ }
533
+ /**
534
+ * 启动定时任务
535
+ */
536
+ startInterval() {
537
+ if (!this.running || this.intervalId !== void 0) {
538
+ return;
539
+ }
540
+ this.intervalId = setPromiseInterval__default.default(async () => {
541
+ await this.runOnce();
542
+ }, BASE_INTERVAL);
543
+ }
544
+ /**
545
+ * 暂停定时任务,延迟后重启(用于 429 限流)
546
+ */
547
+ pauseAndRestart(delayMs) {
548
+ if (this.intervalId !== void 0) {
549
+ setPromiseInterval.clearPromiseInterval(this.intervalId);
550
+ this.intervalId = void 0;
551
+ }
552
+ this.delayTimeoutId = setTimeout(() => {
553
+ this.delayTimeoutId = void 0;
554
+ if (this.running) {
555
+ this.runOnce().finally(() => {
556
+ this.startInterval();
557
+ });
558
+ }
559
+ }, delayMs);
560
+ }
561
+ /**
562
+ * 停止 Redeemer
563
+ */
564
+ stop() {
565
+ if (this.intervalId !== void 0) {
566
+ setPromiseInterval.clearPromiseInterval(this.intervalId);
567
+ this.intervalId = void 0;
568
+ }
569
+ if (this.delayTimeoutId) {
570
+ clearTimeout(this.delayTimeoutId);
571
+ this.delayTimeoutId = void 0;
572
+ }
573
+ this.running = false;
574
+ init.logger.info("Redeemer 已停止");
575
+ }
576
+ /**
577
+ * 获取可 Redeem 的仓位
578
+ */
579
+ async fetchRedeemablePositions() {
580
+ const url = new URL(`${DATA_API_HOST}/positions`);
581
+ url.searchParams.set("user", this.funderAddress);
582
+ url.searchParams.set("redeemable", "true");
583
+ url.searchParams.set("sizeThreshold", ".1");
584
+ url.searchParams.set("sortBy", "CURRENT");
585
+ url.searchParams.set("sortDirection", "DESC");
586
+ url.searchParams.set("limit", "10");
587
+ const response = await fetch(url.toString());
588
+ if (!response.ok) {
589
+ throw new Error(`获取仓位失败: ${response.status} ${response.statusText}`);
590
+ }
591
+ const data = await response.json();
592
+ const positions = [];
593
+ for (const item of data) {
594
+ const currentValue = item._currentValue || item.currentValue || 0;
595
+ if (currentValue > 0) {
596
+ positions.push({
597
+ conditionId: item.conditionId,
598
+ assetId: item.asset,
599
+ currentValue
600
+ });
601
+ } else {
602
+ break;
603
+ }
604
+ }
605
+ return positions;
606
+ }
607
+ /**
608
+ * 执行一轮 Redeem
609
+ */
610
+ async runOnce() {
611
+ if (!this.running || !this.redeemFn) {
612
+ return;
613
+ }
614
+ let positions;
615
+ try {
616
+ positions = await this.fetchRedeemablePositions();
617
+ } catch (error) {
618
+ const errorMsg = error instanceof Error ? error.message : String(error);
619
+ if (errorMsg.includes("429") || errorMsg.includes("Too Many Requests")) {
620
+ const resetSeconds = this.parseResetSeconds(errorMsg);
621
+ init.logger.warning(`获取仓位限流,${resetSeconds} 秒后重试`);
622
+ notifier.warning("API 限流", `获取仓位限流,${resetSeconds} 秒后重试`);
623
+ this.pauseAndRestart((resetSeconds + 60) * 1e3);
624
+ return;
625
+ }
626
+ init.logger.error(`获取仓位失败: ${errorMsg}`);
627
+ return;
628
+ }
629
+ init.logger.info(`autoRedeem: ${positions.length} 个可 Redeem 仓位`);
630
+ for (const position of positions) {
631
+ let record = this.records.get(position.conditionId);
632
+ if (!record) {
633
+ record = {
634
+ position,
635
+ success: false,
636
+ failedCount: 0,
637
+ matchedCount: 0
638
+ };
639
+ this.records.set(position.conditionId, record);
640
+ }
641
+ record.position = position;
642
+ if (record.success) {
643
+ record.matchedCount++;
644
+ if (record.matchedCount >= MAX_MATCH_TIMES) {
645
+ init.logger.error(
646
+ `Redeem 失败: 执行成功但仍存在 ${record.matchedCount} 次 (conditionId: ${position.conditionId.slice(0, 10)}...)`
647
+ );
648
+ notifier.error(
649
+ "Redeem 失败",
650
+ `执行成功但仓位仍存在 ${record.matchedCount} 次
651
+ conditionId: ${position.conditionId}
652
+ 价值: ${position.currentValue} USDC`
653
+ );
654
+ }
655
+ } else if (record.failedCount < MAX_MATCH_TIMES) {
656
+ try {
657
+ const txHash = await this.redeemFn(position.conditionId);
658
+ record.success = true;
659
+ init.logger.info(`Redeem 交易已提交: ${txHash}`);
660
+ } catch (error) {
661
+ record.failedCount++;
662
+ const errorMsg = error instanceof Error ? error.message : String(error);
663
+ if (errorMsg.includes("429") || errorMsg.includes("Too Many Requests")) {
664
+ const resetSeconds = this.parseResetSeconds(errorMsg);
665
+ init.logger.warning(`Redeem 限流,${resetSeconds} 秒后重试`);
666
+ notifier.warning("Redeem 限流", `${resetSeconds} 秒后重试`);
667
+ this.pauseAndRestart((resetSeconds + 60) * 1e3);
668
+ return;
669
+ }
670
+ init.logger.error(`Redeem 异常 (第 ${record.failedCount} 次): ${errorMsg}`);
671
+ if (record.failedCount >= MAX_MATCH_TIMES) {
672
+ notifier.error(
673
+ "Redeem 异常",
674
+ `重试 ${record.failedCount} 次仍失败
675
+ conditionId: ${position.conditionId}
676
+ 错误: ${errorMsg}`
677
+ );
678
+ }
679
+ }
680
+ }
681
+ }
682
+ const successList = [];
683
+ for (const [conditionId, record] of this.records.entries()) {
684
+ const stillExists = positions.some((p) => p.conditionId === conditionId);
685
+ if (!stillExists) {
686
+ successList.push(
687
+ `${record.position.assetId.slice(0, 10)}... (${record.position.currentValue} USDC, matched: ${record.matchedCount})`
688
+ );
689
+ this.records.delete(conditionId);
690
+ }
691
+ }
692
+ if (successList.length > 0) {
693
+ init.logger.success(`Redeem 成功: ${successList.length} 个`);
694
+ for (const item of successList) {
695
+ init.logger.line("", `- ${item}`);
696
+ }
697
+ this.onRedeemSuccess?.();
698
+ }
699
+ init.logger.info(`redeemedRecord: ${this.records.size} 个待确认`);
700
+ }
701
+ /**
702
+ * 解析 429 错误中的 resets in 时间
703
+ */
704
+ parseResetSeconds(errorMsg) {
705
+ const match = errorMsg.match(/resets in (\d+) seconds/);
706
+ return match ? parseInt(match[1]) : 3600;
707
+ }
708
+ }
709
+ const POSITION_POLL_INTERVAL = 5e3;
710
+ const POSITION_MATCH_THRESHOLD = 0.99;
711
+ class Trader {
712
+ client;
713
+ relayClient;
714
+ assetFilter;
715
+ balanceCache;
716
+ orderWatcher;
717
+ redeemer;
718
+ config = null;
719
+ initialized = false;
720
+ constructor() {
721
+ this.client = new ClobClientWrapper();
722
+ this.relayClient = new RelayClientWrapper();
723
+ this.assetFilter = new AssetFilter();
724
+ this.balanceCache = new BalanceCache();
725
+ this.orderWatcher = new OrderWatcher();
726
+ this.redeemer = new Redeemer();
727
+ }
728
+ /**
729
+ * 初始化交易模块
730
+ */
731
+ async init(config) {
732
+ if (this.initialized) {
733
+ return;
734
+ }
735
+ this.config = config;
736
+ if (!config.polymarket?.privateKey) {
737
+ init.logger.warning("未配置 PolyMarket 私钥,交易功能已禁用");
738
+ return;
739
+ }
740
+ await this.client.init({
741
+ privateKey: config.polymarket.privateKey,
742
+ funderAddress: config.polymarket.funderAddress,
743
+ builderCreds: config.polymarket.builderCreds
744
+ });
745
+ if (config.polymarket.builderCreds) {
746
+ this.relayClient.init({
747
+ privateKey: config.polymarket.privateKey,
748
+ builderCreds: config.polymarket.builderCreds
749
+ });
750
+ } else {
751
+ init.logger.warning("未配置 builderCreds,Redeem 功能已禁用");
752
+ }
753
+ this.initialized = true;
754
+ init.logger.success("交易模块初始化完成");
755
+ }
756
+ /**
757
+ * 启动 Redeemer(在 Telegram listener 就绪后调用)
758
+ */
759
+ startRedeemer() {
760
+ if (!this.config?.polymarket?.builderCreds) {
761
+ return;
762
+ }
763
+ this.redeemer.start(
764
+ this.executeRedeem.bind(this),
765
+ this.config.polymarket.funderAddress || "",
766
+ () => this.balanceCache.invalidate()
767
+ );
768
+ }
769
+ /**
770
+ * 检查是否已初始化
771
+ */
772
+ isInitialized() {
773
+ return this.initialized;
774
+ }
775
+ /**
776
+ * 处理预测信号
777
+ */
778
+ async executeSignal(signal) {
779
+ if (!this.initialized || !this.config) {
780
+ init.logger.warning("交易模块未初始化,跳过信号");
781
+ return;
782
+ }
783
+ const { metadata } = signal;
784
+ const { assetId, endTime, outcome } = metadata;
785
+ init.logger.section(`处理信号: ${outcome} (${metadata.eventId})`);
786
+ if (!this.assetFilter.canTrade(assetId, endTime)) {
787
+ init.logger.warning(`assetId 已交易,跳过: ${assetId.slice(0, 10)}...`);
788
+ return;
789
+ }
790
+ const tradingConfig = this.config.trading;
791
+ const amountMode = tradingConfig?.amountMode || "fixed";
792
+ const amountValue = tradingConfig?.amountValue || 1;
793
+ const buyPrice = tradingConfig?.buyPrice || 0.98;
794
+ const sellPrice = tradingConfig?.sellPrice || 1;
795
+ let balance;
796
+ try {
797
+ balance = await this.balanceCache.getBalance(
798
+ endTime,
799
+ () => this.client.getBalance()
800
+ );
801
+ } catch (error) {
802
+ const errorMsg = error instanceof Error ? error.message : String(error);
803
+ init.logger.error(`获取余额失败: ${errorMsg}`);
804
+ return;
805
+ }
806
+ if (amountMode === "fixed" && balance < amountValue) {
807
+ const msg = `余额不足: ${balance} USDC < ${amountValue} USDC`;
808
+ init.logger.warning(msg);
809
+ notifier.warning("余额不足", `当前余额: ${balance} USDC
810
+ 需要金额: ${amountValue} USDC`);
811
+ return;
812
+ }
813
+ if (amountMode === "percentage" && balance <= 0) {
814
+ init.logger.warning(`余额为零: ${balance} USDC`);
815
+ notifier.warning("余额为零", `当前余额: ${balance} USDC`);
816
+ return;
817
+ }
818
+ const orderAmount = amountMode === "fixed" ? amountValue : balance * amountValue;
819
+ const size = orderAmount / buyPrice;
820
+ init.logger.info(`下单金额: ${orderAmount.toFixed(2)} USDC`);
821
+ init.logger.info(`下单价格: ${buyPrice}`);
822
+ init.logger.info(`下单数量: ${size.toFixed(4)}`);
823
+ const buyResult = await this.client.createBuyOrder(assetId, buyPrice, size);
824
+ if (!buyResult.success) {
825
+ init.logger.error(`买单失败: ${buyResult.errorMsg}`);
826
+ notifier.error("买单失败", `事件: ${metadata.eventId}
827
+ 错误: ${buyResult.errorMsg}`);
828
+ return;
829
+ }
830
+ this.assetFilter.markTraded(assetId, endTime);
831
+ this.watchAndHandle(buyResult.orderId, assetId, sellPrice, endTime, metadata.eventId);
832
+ }
833
+ /**
834
+ * 异步监控订单并处理结果(不阻塞主流程)
835
+ */
836
+ async watchAndHandle(orderId, assetId, sellPrice, endTime, eventId) {
837
+ try {
838
+ const watchResult = await this.orderWatcher.watch(
839
+ orderId,
840
+ endTime,
841
+ (id) => this.client.getOrder(id)
842
+ );
843
+ await this.handleBuyResult(watchResult, orderId, assetId, sellPrice, endTime);
844
+ } catch (error) {
845
+ const errorMsg = error instanceof Error ? error.message : String(error);
846
+ init.logger.error(`订单监控异常: ${errorMsg}`);
847
+ notifier.error("订单监控异常", `事件: ${eventId}
848
+ 订单: ${orderId}
849
+ 错误: ${errorMsg}`);
850
+ }
851
+ }
852
+ /**
853
+ * 处理买单结果
854
+ */
855
+ async handleBuyResult(result, orderId, assetId, sellPrice, endTime) {
856
+ switch (result.status) {
857
+ case "MATCHED":
858
+ case "CONFIRMED":
859
+ const filledSize = result.filledSize || 0;
860
+ init.logger.success(`买单成交: ${filledSize.toFixed(4)} 份`);
861
+ if (sellPrice < 1) {
862
+ this.waitAndSell(assetId, filledSize, sellPrice, endTime);
863
+ } else {
864
+ init.logger.info("sellPrice = 1,等待 Redeem");
865
+ }
866
+ break;
867
+ case "CANCELLED":
868
+ init.logger.info("订单已取消(市场可能已结束)");
869
+ break;
870
+ case "FAILED":
871
+ init.logger.error("订单执行失败");
872
+ notifier.error("订单执行失败", `订单 ID: ${orderId}`);
873
+ break;
874
+ case "TIMEOUT":
875
+ init.logger.warning("订单超时,尝试取消");
876
+ notifier.warning("订单超时", `订单 ID: ${orderId}
877
+ 已尝试取消`);
878
+ await this.client.cancelOrder(orderId);
879
+ break;
880
+ }
881
+ }
882
+ /**
883
+ * 等待仓位同步后执行卖出(独立轮询)
884
+ */
885
+ async waitAndSell(assetId, expectedSize, sellPrice, endTime) {
886
+ const shortId = assetId.substring(0, 10) + "...";
887
+ const threshold = expectedSize * POSITION_MATCH_THRESHOLD;
888
+ init.logger.info(`等待仓位同步: ${shortId},预期 >= ${threshold.toFixed(4)}`);
889
+ while (true) {
890
+ const now = Date.now();
891
+ if (now >= endTime) {
892
+ init.logger.warning(`仓位同步超时: ${shortId},等待 Redeem`);
893
+ this.notifyRedeemFallback(assetId, expectedSize);
894
+ return;
895
+ }
896
+ try {
897
+ const positionSize = await this.client.getPositionSize(assetId);
898
+ if (positionSize >= threshold) {
899
+ init.logger.info(`仓位已同步: ${shortId},数量: ${positionSize.toFixed(4)}`);
900
+ await this.executeSellOrder(assetId, sellPrice, positionSize);
901
+ return;
902
+ }
903
+ } catch (error) {
904
+ const errorMsg = error instanceof Error ? error.message : String(error);
905
+ init.logger.warning(`获取仓位失败: ${shortId},${errorMsg}`);
906
+ }
907
+ await this.delay(POSITION_POLL_INTERVAL);
908
+ }
909
+ }
910
+ /**
911
+ * 通知需要 Redeem 兜底
912
+ */
913
+ notifyRedeemFallback(assetId, expectedSize) {
914
+ if (!this.relayClient.isInitialized()) {
915
+ const msg = "仓位同步超时且 Redeem 不可用";
916
+ init.logger.warning(msg);
917
+ notifier.warning(
918
+ msg,
919
+ `Asset: ${assetId.substring(0, 20)}...
920
+ 预期数量: ${expectedSize.toFixed(4)}
921
+ 请手动处理或配置 Builder API`
922
+ );
923
+ }
924
+ }
925
+ /**
926
+ * 延迟
927
+ */
928
+ delay(ms) {
929
+ return new Promise((resolve) => setTimeout(resolve, ms));
930
+ }
931
+ /**
932
+ * 执行卖单
933
+ */
934
+ async executeSellOrder(assetId, price, size) {
935
+ init.logger.info(`创建卖单: ${size.toFixed(4)} 份 @ ${price}`);
936
+ const sellResult = await this.client.createSellOrder(assetId, price, size);
937
+ if (sellResult.success) {
938
+ init.logger.success(`卖单已创建: ${sellResult.orderId}`);
939
+ } else {
940
+ init.logger.error(`卖单创建失败: ${sellResult.errorMsg}`);
941
+ notifier.error("卖单创建失败", `错误: ${sellResult.errorMsg}`);
942
+ }
943
+ }
944
+ /**
945
+ * 执行 Redeem(供 Redeemer 调用)
946
+ */
947
+ async executeRedeem(conditionId) {
948
+ if (!this.relayClient.isInitialized()) {
949
+ throw new Error("RelayClient 未初始化");
950
+ }
951
+ return await this.relayClient.redeem(conditionId);
952
+ }
953
+ /**
954
+ * 停止交易模块
955
+ */
956
+ async shutdown() {
957
+ this.redeemer.stop();
958
+ init.logger.info("交易模块已停止");
959
+ }
960
+ }
961
+ const trader = new Trader();
962
+ async function main() {
963
+ init.logger.init();
964
+ init.logger.section("PolyMarket 跟单机器人");
965
+ const config = await init.ensureConfig();
966
+ notifier.init(config.telegram.botToken, config.telegram.adminChatId);
967
+ const autoTradeEnabled = init.canAutoTrade(config);
968
+ if (!autoTradeEnabled) {
969
+ init.logger.warning("自动跟单功能未启用");
970
+ if (!config.polymarket?.privateKey) {
971
+ init.logger.line("", "- PolyMarket 配置不完整(缺少私钥)");
972
+ }
973
+ if (!config.trading?.amountMode || !config.trading?.amountValue || !config.trading?.buyPrice || !config.trading?.sellPrice) {
974
+ init.logger.line("", "- 交易配置不完整");
975
+ }
976
+ init.logger.line("", "当前不会跟单任何预测消息");
977
+ } else {
978
+ try {
979
+ await trader.init(config);
980
+ if (config.trading?.sellPrice === 1 && !config.polymarket?.builderCreds) {
981
+ init.logger.warning("sellPrice = 1 但未配置 builderCreds,持仓将无法自动结算");
982
+ init.logger.line("", "请配置 Builder API 凭证,或手动 claim");
983
+ }
984
+ } catch (error) {
985
+ const errorMsg = error instanceof Error ? error.message : String(error);
986
+ init.logger.error(`交易模块初始化失败: ${errorMsg}`);
987
+ init.logger.line("", "当前不会跟单任何预测消息");
988
+ }
989
+ }
990
+ const listener = new init.TelegramListener(
991
+ config.telegram.botToken,
992
+ config.telegram.targetChatIds || []
993
+ );
994
+ listener.onChatIdsChanged(async (chatIds) => {
995
+ const telegram = init.configLocal.getItem("telegram");
996
+ if (!telegram) {
997
+ init.logger.error("警告: 无法保存频道 ID,telegram 配置不存在");
998
+ return;
999
+ }
1000
+ telegram.targetChatIds = chatIds.length > 0 ? chatIds : void 0;
1001
+ init.configLocal.setItem("telegram", telegram);
1002
+ init.logger.line("💾", `已保存频道 ID 列表: ${chatIds.length} 个频道`);
1003
+ });
1004
+ listener.onSignal(async (signal) => {
1005
+ init.logger.info(`📨 收到预测信号:`);
1006
+ init.logger.line("", `事件: ${signal.metadata.eventId}`);
1007
+ init.logger.line("", `方向: ${signal.metadata.outcome === "Up" ? "上涨 📈" : "下跌 📉"}`);
1008
+ init.logger.line("", `Asset: ${signal.metadata.assetId}`);
1009
+ init.logger.line("", `结束时间: ${new Date(signal.metadata.endTime).toLocaleString()}`);
1010
+ if (trader.isInitialized()) {
1011
+ try {
1012
+ await trader.executeSignal(signal);
1013
+ } catch (error) {
1014
+ const errorMsg = error instanceof Error ? error.message : String(error);
1015
+ init.logger.error(`交易执行失败: ${errorMsg}`);
1016
+ }
1017
+ }
1018
+ });
1019
+ let isShuttingDown = false;
1020
+ const shutdown = () => {
1021
+ if (isShuttingDown) return;
1022
+ isShuttingDown = true;
1023
+ process.removeAllListeners("SIGINT");
1024
+ process.removeAllListeners("SIGTERM");
1025
+ init.logger.blank();
1026
+ init.logger.info("正在退出...");
1027
+ setTimeout(() => {
1028
+ process.exit(0);
1029
+ }, 1e3);
1030
+ Promise.all([
1031
+ trader.shutdown(),
1032
+ listener.stop()
1033
+ ]).finally(() => {
1034
+ process.exit(0);
1035
+ });
1036
+ };
1037
+ process.on("SIGINT", shutdown);
1038
+ process.on("SIGTERM", shutdown);
1039
+ await listener.start(() => {
1040
+ if (trader.isInitialized()) {
1041
+ trader.startRedeemer();
1042
+ }
1043
+ });
1044
+ }
1045
+ main().catch((error) => {
1046
+ init.logger.error("程序运行出错:", error);
1047
+ process.exit(1);
1048
+ });