polycopy 0.1.8 → 0.2.0

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/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const child_process = require("child_process");
4
4
  const fs = require("fs");
5
5
  const path = require("path");
6
- const processLock = require("./process-lock-BbFF27h8.js");
6
+ const processLock = require("./process-lock-y_yjnMCr.js");
7
7
  function _interopNamespace(e) {
8
8
  if (e && e.__esModule) return e;
9
9
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- const processLock = require("./process-lock-BbFF27h8.js");
2
+ const processLock = require("./process-lock-y_yjnMCr.js");
3
3
  const grammy = require("grammy");
4
4
  const log4js = require("log4js");
5
5
  const path = require("path");
@@ -874,15 +874,7 @@ class RelayClientWrapper {
874
874
  isInitialized() {
875
875
  return this.initialized;
876
876
  }
877
- /**
878
- * 执行 Redeem
879
- * @param conditionId 条件 ID
880
- * @returns 交易哈希
881
- */
882
- async redeem(conditionId) {
883
- if (!this.relayClient || !this.initialized) {
884
- throw new Error("RelayClient 未初始化");
885
- }
877
+ buildRedeemTransaction(conditionId) {
886
878
  const calldata = viem.encodeFunctionData({
887
879
  abi: [
888
880
  {
@@ -901,14 +893,27 @@ class RelayClientWrapper {
901
893
  functionName: "redeemPositions",
902
894
  args: [USDC_ADDRESS, viem.zeroHash, conditionId, [1n, 2n]]
903
895
  });
904
- const tx = {
896
+ return {
905
897
  to: CTF_ADDRESS,
906
898
  value: "0",
907
899
  data: calldata
908
900
  };
901
+ }
902
+ /**
903
+ * 批量执行 Redeem
904
+ * @param conditionIds 条件 ID 列表
905
+ * @returns 交易哈希
906
+ */
907
+ async redeemBatch(conditionIds) {
908
+ if (!this.relayClient || !this.initialized) {
909
+ throw new Error("RelayClient 未初始化");
910
+ }
911
+ const transactions = conditionIds.map(
912
+ (conditionId) => this.buildRedeemTransaction(conditionId)
913
+ );
909
914
  const response = await this.relayClient.execute(
910
- [tx],
911
- "Polymarket redeem positions"
915
+ transactions,
916
+ `Polymarket redeem positions (${transactions.length})`
912
917
  );
913
918
  const result = await response.wait();
914
919
  if (!result) {
@@ -918,6 +923,14 @@ class RelayClientWrapper {
918
923
  }
919
924
  return result.transactionHash;
920
925
  }
926
+ /**
927
+ * 执行 Redeem(兼容单个)
928
+ * @param conditionId 条件 ID
929
+ * @returns 交易哈希
930
+ */
931
+ async redeem(conditionId) {
932
+ return this.redeemBatch([conditionId]);
933
+ }
921
934
  }
922
935
  class AssetFilter {
923
936
  // endTime -> Set<assetId>
@@ -1059,6 +1072,7 @@ class OrderWatcher {
1059
1072
  }
1060
1073
  const DATA_API_HOST = "https://data-api.polymarket.com";
1061
1074
  const BASE_INTERVAL = 5 * 60 * 1e3;
1075
+ const DEFAULT_BATCH_SIZE = 5;
1062
1076
  const MAX_MATCH_TIMES = 3;
1063
1077
  class Redeemer {
1064
1078
  // conditionId -> RedeemRecord
@@ -1069,6 +1083,7 @@ class Redeemer {
1069
1083
  state = "stopped";
1070
1084
  scheduler;
1071
1085
  redeemFn = null;
1086
+ batchSize = DEFAULT_BATCH_SIZE;
1072
1087
  funderAddress = "";
1073
1088
  onRedeemSuccess = null;
1074
1089
  constructor(scheduler = {
@@ -1090,6 +1105,10 @@ class Redeemer {
1090
1105
  return;
1091
1106
  }
1092
1107
  this.redeemFn = redeemFn;
1108
+ const envBatchSize = Number(process.env.POLYCOPY_REDEEM_BATCH_SIZE || "");
1109
+ if (!Number.isNaN(envBatchSize) && envBatchSize > 0) {
1110
+ this.batchSize = Math.floor(envBatchSize);
1111
+ }
1093
1112
  this.funderAddress = funderAddress;
1094
1113
  this.onRedeemSuccess = onRedeemSuccess || null;
1095
1114
  this.state = "running";
@@ -1203,6 +1222,24 @@ class Redeemer {
1203
1222
  logger.error(`获取仓位失败: ${errorMsg}`);
1204
1223
  return;
1205
1224
  }
1225
+ const currentIds = new Set(positions.map((position) => position.conditionId));
1226
+ this.logPendingPositions(positions);
1227
+ const pendingRecords = this.buildRecords(positions);
1228
+ this.updateMatchedRecords(currentIds);
1229
+ const shouldContinue = await this.executeBatches(pendingRecords);
1230
+ if (!shouldContinue) {
1231
+ return;
1232
+ }
1233
+ const successItems = this.collectSuccessRecords(currentIds);
1234
+ if (successItems.length > 0) {
1235
+ const successTitle = `自动 redeem: 成功 ${successItems.length} 个`;
1236
+ const successLines = this.logPositions(successTitle, successItems);
1237
+ telegramService.success(successTitle, successLines.join("\n"));
1238
+ this.onRedeemSuccess?.();
1239
+ }
1240
+ logger.info(`redeem 状态: ${this.records.size} 个待确认`);
1241
+ }
1242
+ logPendingPositions(positions) {
1206
1243
  const pendingRedeemablePositions = positions.filter((position) => {
1207
1244
  const record = this.records.get(position.conditionId);
1208
1245
  return !record?.success;
@@ -1212,8 +1249,11 @@ class Redeemer {
1212
1249
  `自动 redeem: 发现 ${pendingRedeemablePositions.length} 个可 Redeem 仓位`,
1213
1250
  pendingRedeemablePositions.map((position) => ({ position }))
1214
1251
  );
1252
+ logger.info(`Redeem 批量大小: ${this.batchSize}`);
1215
1253
  }
1216
- for (const position of positions) {
1254
+ }
1255
+ buildRecords(positions) {
1256
+ return positions.map((position) => {
1217
1257
  let record = this.records.get(position.conditionId);
1218
1258
  if (!record) {
1219
1259
  record = {
@@ -1225,52 +1265,74 @@ class Redeemer {
1225
1265
  this.records.set(position.conditionId, record);
1226
1266
  }
1227
1267
  record.position = position;
1228
- if (record.success) {
1229
- record.matchedCount++;
1230
- if (record.matchedCount >= MAX_MATCH_TIMES) {
1231
- logger.error(
1232
- `Redeem 失败: 执行成功但仍存在 ${record.matchedCount} (conditionId: ${position.conditionId.slice(0, 10)}...)`
1233
- );
1268
+ return record;
1269
+ }).filter((record) => !record.success && record.failedCount < MAX_MATCH_TIMES);
1270
+ }
1271
+ updateMatchedRecords(currentIds) {
1272
+ for (const record of this.records.values()) {
1273
+ if (!record.success) {
1274
+ continue;
1275
+ }
1276
+ if (!currentIds.has(record.position.conditionId)) {
1277
+ continue;
1278
+ }
1279
+ record.matchedCount++;
1280
+ if (record.matchedCount >= MAX_MATCH_TIMES) {
1281
+ logger.error(
1282
+ `Redeem 失败: 执行成功但仍存在 ${record.matchedCount} 次 (conditionId: ${record.position.conditionId.slice(0, 10)}...)`
1283
+ );
1284
+ telegramService.error(
1285
+ "Redeem 失败",
1286
+ `执行成功但仓位仍存在 ${record.matchedCount} 次
1287
+ conditionId: ${record.position.conditionId}
1288
+ 价值: ${record.position.currentValue} USDC`
1289
+ );
1290
+ }
1291
+ }
1292
+ }
1293
+ async executeBatches(pendingRecords) {
1294
+ const batch = pendingRecords.slice(0, this.batchSize);
1295
+ const conditionIds = batch.map((record) => record.position.conditionId);
1296
+ if (conditionIds.length === 0) {
1297
+ return true;
1298
+ }
1299
+ try {
1300
+ const txHash = await this.redeemFn?.(conditionIds);
1301
+ batch.forEach((record) => {
1302
+ record.success = true;
1303
+ record.matchedCount = 0;
1304
+ });
1305
+ logger.info(`Redeem 已执行 (批量 ${batch.length}): ${txHash}`);
1306
+ } catch (error) {
1307
+ const errorMsg = error instanceof Error ? error.message : String(error);
1308
+ if (errorMsg.includes("429") || errorMsg.includes("Too Many Requests")) {
1309
+ const resetSeconds = this.parseResetSeconds(errorMsg);
1310
+ logger.warning(`Redeem 限流,${resetSeconds} 秒后重试`);
1311
+ telegramService.warning("Redeem 限流", `${resetSeconds} 秒后重试`);
1312
+ this.pauseAndRestart((resetSeconds + 60) * 1e3);
1313
+ return false;
1314
+ }
1315
+ batch.forEach((record) => {
1316
+ record.failedCount++;
1317
+ });
1318
+ logger.error(`Redeem 批量异常 (批量 ${batch.length}): ${errorMsg}`);
1319
+ batch.forEach((record) => {
1320
+ if (record.failedCount >= MAX_MATCH_TIMES) {
1234
1321
  telegramService.error(
1235
- "Redeem 失败",
1236
- `执行成功但仓位仍存在 ${record.matchedCount}
1237
- conditionId: ${position.conditionId}
1238
- 价值: ${position.currentValue} USDC`
1239
- );
1240
- }
1241
- } else if (record.failedCount < MAX_MATCH_TIMES) {
1242
- try {
1243
- const txHash = await this.redeemFn?.(position.conditionId);
1244
- record.success = true;
1245
- logger.info(`Redeem 已执行: ${txHash}`);
1246
- } catch (error) {
1247
- const errorMsg = error instanceof Error ? error.message : String(error);
1248
- if (errorMsg.includes("429") || errorMsg.includes("Too Many Requests")) {
1249
- const resetSeconds = this.parseResetSeconds(errorMsg);
1250
- logger.warning(`Redeem 限流,${resetSeconds} 秒后重试`);
1251
- telegramService.warning("Redeem 限流", `${resetSeconds} 秒后重试`);
1252
- this.pauseAndRestart((resetSeconds + 60) * 1e3);
1253
- return;
1254
- }
1255
- record.failedCount++;
1256
- logger.error(
1257
- `Redeem 异常 (第 ${record.failedCount} 次): ${errorMsg}`
1258
- );
1259
- if (record.failedCount >= MAX_MATCH_TIMES) {
1260
- telegramService.error(
1261
- "Redeem 异常",
1262
- `重试 ${record.failedCount} 次仍失败
1263
- conditionId: ${position.conditionId}
1322
+ "Redeem 异常",
1323
+ `重试 ${record.failedCount} 次仍失败
1324
+ conditionId: ${record.position.conditionId}
1264
1325
  错误: ${errorMsg}`
1265
- );
1266
- }
1326
+ );
1267
1327
  }
1268
- }
1328
+ });
1269
1329
  }
1330
+ return true;
1331
+ }
1332
+ collectSuccessRecords(currentIds) {
1270
1333
  const successItems = [];
1271
1334
  for (const [conditionId, record] of this.records.entries()) {
1272
- const stillExists = positions.some((p) => p.conditionId === conditionId);
1273
- if (!stillExists) {
1335
+ if (!currentIds.has(conditionId)) {
1274
1336
  successItems.push({
1275
1337
  position: record.position,
1276
1338
  matchedCount: record.matchedCount
@@ -1278,13 +1340,7 @@ conditionId: ${position.conditionId}
1278
1340
  this.records.delete(conditionId);
1279
1341
  }
1280
1342
  }
1281
- if (successItems.length > 0) {
1282
- const successTitle = `自动 redeem: 成功 ${successItems.length} 个`;
1283
- const successLines = this.logPositions(successTitle, successItems);
1284
- telegramService.success(successTitle, successLines.join("\n"));
1285
- this.onRedeemSuccess?.();
1286
- }
1287
- logger.info(`redeem 状态: ${this.records.size} 个待确认`);
1343
+ return successItems;
1288
1344
  }
1289
1345
  /**
1290
1346
  * 统一格式化仓位信息
@@ -1371,7 +1427,7 @@ class Trader {
1371
1427
  return;
1372
1428
  }
1373
1429
  this.redeemer.start(
1374
- this.executeRedeem.bind(this),
1430
+ this.executeRedeemBatch.bind(this),
1375
1431
  this.config.polymarket.funderAddress || "",
1376
1432
  () => this.balanceCache.invalidate()
1377
1433
  );
@@ -1577,13 +1633,13 @@ class Trader {
1577
1633
  }
1578
1634
  }
1579
1635
  /**
1580
- * 执行 Redeem(供 Redeemer 调用)
1636
+ * 批量执行 Redeem(供 Redeemer 调用)
1581
1637
  */
1582
- async executeRedeem(conditionId) {
1638
+ async executeRedeemBatch(conditionIds) {
1583
1639
  if (!this.relayClient.isInitialized()) {
1584
1640
  throw new Error("RelayClient 未初始化");
1585
1641
  }
1586
- return this.relayClient.redeem(conditionId);
1642
+ return this.relayClient.redeemBatch(conditionIds);
1587
1643
  }
1588
1644
  /**
1589
1645
  * 停止交易模块
@@ -1654,8 +1710,12 @@ ${message}`);
1654
1710
  process.on("beforeExit", async () => {
1655
1711
  await logger.flush();
1656
1712
  });
1657
- logger.section("PolyMarket 跟单机器人");
1658
1713
  const config = await processLock.ensureConfig();
1714
+ if (process.env.POLYCOPY_DAEMON === "1") {
1715
+ enterDaemonMode();
1716
+ return;
1717
+ }
1718
+ logger.section("PolyMarket 跟单机器人");
1659
1719
  telegramService.init(
1660
1720
  config.telegram.botToken,
1661
1721
  config.telegram.adminChatId,
@@ -1721,10 +1781,6 @@ ${message}`);
1721
1781
  }
1722
1782
  }
1723
1783
  });
1724
- if (process.env.POLYCOPY_DAEMON === "1") {
1725
- enterDaemonMode();
1726
- return;
1727
- }
1728
1784
  let isShuttingDown = false;
1729
1785
  const shutdown = async () => {
1730
1786
  if (isShuttingDown) return;
@@ -467,12 +467,13 @@ async function ensureConfig() {
467
467
  }
468
468
  return config;
469
469
  }
470
+ const normalizePath = (value) => value.replace(/\\/g, "/").toLowerCase();
471
+ const distIndexPath = normalizePath(DIST_INDEX);
472
+ const distCliPath = normalizePath(DIST_CLI);
470
473
  const normalizeCommand = (command) => command.replace(/\\/g, "/");
471
474
  const isPolycopyCommand = (command) => {
472
475
  const normalized = normalizeCommand(command).toLowerCase();
473
- const distIndexMarker = "dist/index.js";
474
- const distCliMarker = "dist/cli.js";
475
- return normalized.includes(DIST_INDEX.toLowerCase()) || normalized.includes(DIST_CLI.toLowerCase()) || normalized.includes(distIndexMarker) || normalized.includes(distCliMarker);
476
+ return normalized.includes(distIndexPath) || normalized.includes(distCliPath) || normalized.includes("dist/cli.js");
476
477
  };
477
478
  const findPolycopyPids = () => {
478
479
  if (process.platform === "win32") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polycopy",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "polycopy test",
5
5
  "main": "dist/index.js",
6
6
  "bin": {