polycopy 0.2.6 → 0.2.7

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.
Files changed (2) hide show
  1. package/dist/index.js +372 -249
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -10,8 +10,8 @@ const viem = require("viem");
10
10
  const accounts = require("viem/accounts");
11
11
  const chains = require("viem/chains");
12
12
  const builderRelayerClient = require("@polymarket/builder-relayer-client");
13
+ const EventEmitter = require("eventemitter3");
13
14
  const setPromiseInterval = require("set-promise-interval");
14
- const lodash = require("lodash");
15
15
  const child_process = require("child_process");
16
16
  const fs = require("fs");
17
17
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
@@ -34,10 +34,10 @@ function _interopNamespace(e) {
34
34
  }
35
35
  const log4js__default = /* @__PURE__ */ _interopDefault(log4js);
36
36
  const path__namespace = /* @__PURE__ */ _interopNamespace(path);
37
+ const EventEmitter__default = /* @__PURE__ */ _interopDefault(EventEmitter);
37
38
  const setPromiseInterval__default = /* @__PURE__ */ _interopDefault(setPromiseInterval);
38
39
  const fs__namespace = /* @__PURE__ */ _interopNamespace(fs);
39
40
  const LOG_FILE = path__namespace.join(processLock.LOGS_DIR, "polycopy.log");
40
- path__namespace.join(processLock.LOGS_DIR, "polycopy");
41
41
  log4js__default.default.configure({
42
42
  appenders: {
43
43
  file: {
@@ -61,7 +61,6 @@ log4js__default.default.configure({
61
61
  }
62
62
  });
63
63
  const log4jsLogger = log4js__default.default.getLogger("polycopy");
64
- const formatMessage = (message, args) => args.length > 0 ? `${message} ${args.join(" ")}` : message;
65
64
  class Logger {
66
65
  initialized = false;
67
66
  /**
@@ -74,31 +73,31 @@ class Logger {
74
73
  📁 日志目录: ${processLock.LOGS_DIR}`);
75
74
  }
76
75
  /**
77
- * 输出信息日志(前后自动添加空行)
76
+ * 输出信息日志
78
77
  */
79
- info(message, ...args) {
80
- log4jsLogger.info(formatMessage(message, args));
78
+ info(...messages) {
79
+ log4jsLogger.info(messages[0], ...messages.slice(1));
81
80
  }
82
81
  /**
83
- * 输出成功日志(前后自动添加空行)
82
+ * 输出成功日志
84
83
  */
85
- success(message, ...args) {
86
- log4jsLogger.info(`✅ ${formatMessage(message, args)}`);
84
+ success(...messages) {
85
+ log4jsLogger.info("✅", ...messages);
87
86
  }
88
87
  /**
89
- * 输出警告日志(前后自动添加空行)
88
+ * 输出警告日志
90
89
  */
91
- warning(message, ...args) {
92
- log4jsLogger.warn(`⚠️ ${formatMessage(message, args)}`);
90
+ warning(...messages) {
91
+ log4jsLogger.warn("⚠️", ...messages);
93
92
  }
94
93
  /**
95
- * 输出错误日志(前后自动添加空行)
94
+ * 输出错误日志
96
95
  */
97
- error(message, ...args) {
98
- log4jsLogger.error(`❌ ${formatMessage(message, args)}`);
96
+ error(...messages) {
97
+ log4jsLogger.error("❌", ...messages);
99
98
  }
100
99
  /**
101
- * 输出章节标题(前后自动添加空行)
100
+ * 输出章节标题
102
101
  */
103
102
  section(title) {
104
103
  log4jsLogger.info(`=== ${title} ===`);
@@ -111,12 +110,6 @@ class Logger {
111
110
  const formatted = `${indentStr}${message}`;
112
111
  log4jsLogger.info(formatted);
113
112
  }
114
- /**
115
- * 输出普通文本(不添加空行,用于连续输出)
116
- */
117
- text(message, ...args) {
118
- log4jsLogger.info(formatMessage(message, args));
119
- }
120
113
  /**
121
114
  * 输出空行
122
115
  */
@@ -124,13 +117,16 @@ class Logger {
124
117
  console.log("");
125
118
  }
126
119
  /**
127
- * 输出带前缀的信息(不添加空行)
120
+ * 输出带前缀的信息
128
121
  * 用于在同一组日志中输出多行信息
129
122
  */
130
123
  line(prefix, message, ...args) {
131
124
  const formatted = args.length > 0 ? ` ${prefix} ${message} ${args.join(" ")}` : ` ${prefix} ${message}`;
132
125
  log4jsLogger.info(formatted);
133
126
  }
127
+ lines(...messages) {
128
+ for (let msg of messages) this.info(msg);
129
+ }
134
130
  /**
135
131
  * 刷新日志输出(用于进程退出前)
136
132
  */
@@ -845,43 +841,147 @@ class ClobClientWrapper {
845
841
  return parseFloat(result.balance) / 1e6;
846
842
  }
847
843
  }
848
- const RPC_URL = "https://polygon-rpc.com";
849
844
  const CTF_ADDRESS = viem.getAddress("0x4d97dcd97ec945f40cf65f87097ace5ea0476045");
850
845
  const USDC_ADDRESS = viem.getAddress("0x2791bca1f2de4661ed88a30c99a7a9449aa84174");
851
- class RelayClientWrapper {
852
- relayClient = null;
853
- initialized = false;
854
- /**
855
- * 初始化 RelayClient
856
- */
857
- init(config) {
858
- if (this.initialized) {
846
+ const SUBMIT_ERROR_SYMBOL = "__SUBMIT_ERROR__";
847
+ class RelayClient extends EventEmitter__default.default {
848
+ lastPostTime;
849
+ resetsTime;
850
+ postTimeout = null;
851
+ pendingTransactions = [];
852
+ options;
853
+ constructor(options) {
854
+ super();
855
+ this.options = {
856
+ batchSize: options.batchSize || 10,
857
+ postInterval: options.postInterval || 7.5 * 60 * 1e3,
858
+ maxRetry: 3,
859
+ ...options
860
+ };
861
+ }
862
+ testRequestLimit(error) {
863
+ const errorMsg = error instanceof Error ? error.message : String(error);
864
+ if (errorMsg.includes("429") || errorMsg.includes("Too Many Requests")) {
865
+ const match = errorMsg.match(/resets in (\d+) seconds/);
866
+ return match ? parseInt(match[1]) * 1e3 : 0;
867
+ }
868
+ return 0;
869
+ }
870
+ makePostData() {
871
+ const total = this.pendingTransactions.length;
872
+ const transactions = this.pendingTransactions.sort((a, b) => a.retryNum - b.retryNum).splice(0, this.options.batchSize);
873
+ if (transactions.length === 0) return null;
874
+ const postTransactions = [];
875
+ const postConditionIds = {
876
+ redeem: [],
877
+ merge: []
878
+ };
879
+ for (const { type, conditionId, transaction } of transactions) {
880
+ postConditionIds[type].push(conditionId);
881
+ postTransactions.push(transaction);
882
+ }
883
+ return {
884
+ total,
885
+ transactions,
886
+ postConditionIds,
887
+ postTransactions
888
+ };
889
+ }
890
+ exec() {
891
+ if (this.postTimeout || this.pendingTransactions.length === 0 || this.resetsTime) {
859
892
  return;
860
893
  }
861
- const account = accounts.privateKeyToAccount(config.privateKey);
862
- const walletClient = viem.createWalletClient({
863
- account,
864
- chain: chains.polygon,
865
- transport: viem.http(RPC_URL)
866
- });
867
- const builderConfig = new builderSigningSdk.BuilderConfig({
868
- localBuilderCreds: config.builderCreds
869
- });
870
- this.relayClient = new builderRelayerClient.RelayClient(
871
- "https://relayer-v2.polymarket.com",
872
- 137,
873
- walletClient,
874
- builderConfig,
875
- builderRelayerClient.RelayerTxType.SAFE
894
+ const now = Date.now();
895
+ const leftTime = this.lastPostTime ? Math.max(this.options.postInterval - (now - this.lastPostTime), 0) : 0;
896
+ logger.info(
897
+ "RelayClient",
898
+ "next post:",
899
+ new Date(now + leftTime).toLocaleString()
876
900
  );
877
- this.initialized = true;
878
- logger.success("RelayClient 初始化完成");
901
+ this.postTimeout = setTimeout(() => {
902
+ const postData = this.makePostData();
903
+ if (!postData) return;
904
+ const { total, transactions, postTransactions, postConditionIds } = postData;
905
+ logger.info(
906
+ "RelayClient",
907
+ "post transactions:",
908
+ `${transactions.length}/${total}`
909
+ );
910
+ logger.info(` - pending: ${this.pendingTransactions.length}`);
911
+ this.emit("post", postConditionIds);
912
+ const onPosted = (txHash, error) => {
913
+ this.lastPostTime = Date.now();
914
+ const postedRet = {
915
+ txHash,
916
+ error,
917
+ redeem: {
918
+ success: [],
919
+ failed: []
920
+ },
921
+ merge: {
922
+ success: [],
923
+ failed: []
924
+ }
925
+ };
926
+ const resolveTx = txHash ? (pt) => {
927
+ pt.resolve(txHash);
928
+ const success = postedRet[pt.type].success;
929
+ success.push(pt.conditionId);
930
+ } : (pt) => {
931
+ if (pt.retryNum >= this.options.maxRetry) {
932
+ pt.reject(error);
933
+ const failed = postedRet[pt.type].failed;
934
+ failed.push(pt.conditionId);
935
+ } else {
936
+ this.pendingTransactions.push(pt);
937
+ }
938
+ };
939
+ for (const pt of transactions) {
940
+ pt.retryNum++;
941
+ resolveTx(pt);
942
+ }
943
+ logger.info("RelayClient", "post transactions done:");
944
+ logger.info(
945
+ ` - redeem: [success: ${postedRet.redeem.success.length} failed: ${postedRet.redeem.failed.length}]`
946
+ );
947
+ logger.info(
948
+ ` - merge: [success: ${postedRet.merge.success.length} failed: ${postedRet.merge.failed.length}]`
949
+ );
950
+ logger.info(` - pending: ${this.pendingTransactions.length}`);
951
+ this.emit("posted", postedRet);
952
+ };
953
+ this.post(postTransactions).then((txHash) => onPosted(txHash)).catch((e) => {
954
+ const requestLimit = this.testRequestLimit(e);
955
+ if (requestLimit > 0) {
956
+ this.pendingTransactions.push(...transactions);
957
+ this.resetsTime = Date.now() + requestLimit;
958
+ this.emit("requestLimit", requestLimit);
959
+ setTimeout(() => {
960
+ this.resetsTime = void 0;
961
+ this.emit("resets");
962
+ this.exec();
963
+ }, requestLimit);
964
+ } else {
965
+ onPosted(null, e);
966
+ }
967
+ }).finally(() => {
968
+ this.postTimeout = null;
969
+ this.exec();
970
+ });
971
+ }, leftTime);
879
972
  }
880
- /**
881
- * 检查是否已初始化
882
- */
883
- isInitialized() {
884
- return this.initialized;
973
+ async post(transactions) {
974
+ const response = await this.options.polyRelayClient.execute(
975
+ transactions,
976
+ `Polymarket execute batch (${transactions.length})`
977
+ );
978
+ const result = await response.wait();
979
+ if (!result) {
980
+ throw Error(
981
+ "Redeem 交易失败:wait() 返回空结果(可能被 revert 或 relayer 拒绝)"
982
+ );
983
+ }
984
+ return result.transactionHash;
885
985
  }
886
986
  buildRedeemTransaction(conditionId) {
887
987
  const calldata = viem.encodeFunctionData({
@@ -908,39 +1008,115 @@ class RelayClientWrapper {
908
1008
  data: calldata
909
1009
  };
910
1010
  }
911
- /**
912
- * 批量执行 Redeem
913
- * @param conditionIds 条件 ID 列表
914
- * @returns 交易哈希
915
- */
916
- async redeemBatch(conditionIds) {
917
- if (!this.relayClient || !this.initialized) {
918
- throw new Error("RelayClient 未初始化");
1011
+ buildMergeTransaction(conditionId, amount) {
1012
+ const calldata = viem.encodeFunctionData({
1013
+ abi: [
1014
+ {
1015
+ name: "mergePositions",
1016
+ type: "function",
1017
+ inputs: [
1018
+ { name: "collateralToken", type: "address" },
1019
+ { name: "parentCollectionId", type: "bytes32" },
1020
+ { name: "conditionId", type: "bytes32" },
1021
+ { name: "partition", type: "uint256[]" },
1022
+ { name: "amount", type: "uint256" }
1023
+ ],
1024
+ outputs: [],
1025
+ stateMutability: "nonpayable"
1026
+ }
1027
+ ],
1028
+ functionName: "mergePositions",
1029
+ args: [
1030
+ USDC_ADDRESS,
1031
+ // collateralToken: USDC
1032
+ viem.zeroHash,
1033
+ // parentCollectionId: Polymarket 场景下为 0
1034
+ conditionId,
1035
+ // conditionId: 市场条件 ID
1036
+ [1n, 2n],
1037
+ // partition: Yes(1) 和 No(2)
1038
+ BigInt(amount)
1039
+ // amount: 要合并的 full set 数量(注意是 USDC 的 6 位精度)
1040
+ ]
1041
+ });
1042
+ return {
1043
+ to: CTF_ADDRESS,
1044
+ value: "0",
1045
+ data: calldata
1046
+ };
1047
+ }
1048
+ addTransaction(type, conditionId, transaction) {
1049
+ return new Promise((resolve, reject) => {
1050
+ this.pendingTransactions.push({
1051
+ type,
1052
+ conditionId,
1053
+ transaction,
1054
+ retryNum: 0,
1055
+ reject,
1056
+ resolve
1057
+ });
1058
+ this.exec();
1059
+ logger.info("RelayClient", `add ${type} transaction:${conditionId}`);
1060
+ logger.info(` - pending: ${this.pendingTransactions.length}`);
1061
+ });
1062
+ }
1063
+ checkSubmit(conditionId) {
1064
+ if (this.resetsTime) {
1065
+ throw Error(
1066
+ `request limit before: ${new Date(this.resetsTime).toLocaleString()}`,
1067
+ { cause: SUBMIT_ERROR_SYMBOL }
1068
+ );
1069
+ }
1070
+ if (this.pendingTransactions.some((pt) => pt.conditionId === conditionId)) {
1071
+ throw Error("duplicate conditionId", { cause: SUBMIT_ERROR_SYMBOL });
919
1072
  }
920
- const transactions = conditionIds.map(
921
- (conditionId) => this.buildRedeemTransaction(conditionId)
1073
+ }
1074
+ isSubmitError(error) {
1075
+ return error?.cause === SUBMIT_ERROR_SYMBOL;
1076
+ }
1077
+ async submitRedeem(conditionId) {
1078
+ this.checkSubmit(conditionId);
1079
+ return this.addTransaction(
1080
+ "redeem",
1081
+ conditionId,
1082
+ this.buildRedeemTransaction(conditionId)
922
1083
  );
923
- const response = await this.relayClient.execute(
924
- transactions,
925
- `Polymarket redeem positions (${transactions.length})`
1084
+ }
1085
+ async submitMerge(conditionId, amount) {
1086
+ this.checkSubmit(conditionId);
1087
+ return this.addTransaction(
1088
+ "merge",
1089
+ conditionId,
1090
+ this.buildMergeTransaction(conditionId, amount)
926
1091
  );
927
- const result = await response.wait();
928
- if (!result) {
929
- throw new Error(
930
- "Redeem 交易失败:wait() 返回空结果(可能被 revert 或 relayer 拒绝)"
931
- );
932
- }
933
- return result.transactionHash;
934
1092
  }
935
- /**
936
- * 执行 Redeem(兼容单个)
937
- * @param conditionId 条件 ID
938
- * @returns 交易哈希
939
- */
940
- async redeem(conditionId) {
941
- return this.redeemBatch([conditionId]);
1093
+ clear() {
1094
+ if (this.postTimeout) {
1095
+ clearTimeout(this.postTimeout);
1096
+ }
942
1097
  }
943
1098
  }
1099
+ const RPC_URL = "https://polygon-rpc.com";
1100
+ function initRelayClient(config) {
1101
+ const account = accounts.privateKeyToAccount(config.privateKey);
1102
+ const walletClient = viem.createWalletClient({
1103
+ account,
1104
+ chain: chains.polygon,
1105
+ transport: viem.http(RPC_URL)
1106
+ });
1107
+ const builderConfig = new builderSigningSdk.BuilderConfig({
1108
+ localBuilderCreds: config.builderCreds
1109
+ });
1110
+ const polyRelayClient = new builderRelayerClient.RelayClient(
1111
+ "https://relayer-v2.polymarket.com",
1112
+ 137,
1113
+ walletClient,
1114
+ builderConfig,
1115
+ builderRelayerClient.RelayerTxType.SAFE
1116
+ );
1117
+ logger.success("RelayClient 初始化完成");
1118
+ return new RelayClient({ polyRelayClient });
1119
+ }
944
1120
  class AssetFilter {
945
1121
  // endTime -> Set<assetId>
946
1122
  traded = /* @__PURE__ */ new Map();
@@ -1079,42 +1255,80 @@ class OrderWatcher {
1079
1255
  return new Promise((resolve) => setTimeout(resolve, ms));
1080
1256
  }
1081
1257
  }
1258
+ function listenEvent(e, eventName, callback) {
1259
+ e.addListener(eventName, callback);
1260
+ return () => e.removeListener(eventName, callback);
1261
+ }
1082
1262
  const DATA_API_HOST = "https://data-api.polymarket.com";
1083
- const AUTO_REDEEM_INTERVAL = 8 * 60 * 1e3;
1084
- const MAX_BATCH_SIZE = 10;
1085
- const MAX_MATCH_TIMES = 3;
1086
- class Redeemer {
1087
- // conditionId -> RedeemRecord
1088
- records = /* @__PURE__ */ new Map();
1089
- running = false;
1090
- mainTimer;
1091
- restartTimer;
1263
+ const AUTO_REDEEM_INTERVAL = 60 * 1e3;
1264
+ class Redeemer extends EventEmitter__default.default {
1092
1265
  options;
1266
+ running = false;
1267
+ timer;
1268
+ submitted = /* @__PURE__ */ new Map();
1269
+ unListenRelayClient;
1093
1270
  constructor(options) {
1271
+ super();
1094
1272
  this.options = options;
1095
- }
1096
- start(restart) {
1097
- if (this.running) return;
1098
- this.running = true;
1099
- this.mainTimer = setPromiseInterval__default.default(
1100
- this.autoRedeem.bind(this),
1101
- AUTO_REDEEM_INTERVAL
1273
+ this.unListenRelayClient = this.listenRelayClient();
1274
+ }
1275
+ listenRelayClient() {
1276
+ const { relayClient } = this.options;
1277
+ const unListenPost = listenEvent(relayClient, "post", ({ redeem }) => {
1278
+ logger.lines(
1279
+ `post redeem(待确认):`,
1280
+ ...this.toMessages(this.mapSubmittedPositions(redeem))
1281
+ );
1282
+ });
1283
+ const unListenPosted = listenEvent(
1284
+ relayClient,
1285
+ "posted",
1286
+ ({ redeem, error }) => {
1287
+ const failedPositions = this.mapSubmittedPositions(redeem.failed);
1288
+ if (failedPositions.length > 0) {
1289
+ const messages = [
1290
+ `🔴 redeem 失败已放弃(重试超限): ${failedPositions.length} 个`,
1291
+ ...this.toMessages(failedPositions)
1292
+ ];
1293
+ logger.lines(...messages, error);
1294
+ telegramService.info(messages.join("\n"));
1295
+ }
1296
+ }
1102
1297
  );
1103
- logger.success(restart ? "自动 redeem 已重启" : "自动 redeem 已启动");
1104
- }
1105
- stop() {
1106
- setPromiseInterval.clearPromiseInterval(this.mainTimer);
1107
- clearTimeout(this.restartTimer);
1108
- this.running = false;
1109
- logger.info("自动 redeem 已停止");
1298
+ const unListenRequestLimit = listenEvent(
1299
+ relayClient,
1300
+ "requestLimit",
1301
+ (requestLimit) => {
1302
+ setPromiseInterval.clearPromiseInterval(this.timer);
1303
+ const limitSeconds = requestLimit / 1e3;
1304
+ logger.warning(`Redeem 限流,${limitSeconds} 秒后重试`);
1305
+ telegramService.warning(`Redeem 限流,${limitSeconds} 秒后重试`);
1306
+ }
1307
+ );
1308
+ const unListenResets = listenEvent(relayClient, "resets", () => {
1309
+ this.running = false;
1310
+ this.start();
1311
+ telegramService.info("AutoRedeem 已重启");
1312
+ });
1313
+ return () => {
1314
+ unListenPost();
1315
+ unListenPosted();
1316
+ unListenRequestLimit();
1317
+ unListenResets();
1318
+ };
1110
1319
  }
1111
- restart(delayMs) {
1112
- if (!this.running) return;
1113
- this.stop();
1114
- this.restartTimer = setTimeout(() => {
1115
- this.start(true);
1116
- telegramService.info("自动 redeem 已重启");
1117
- }, delayMs);
1320
+ mapSubmittedPositions(conditions) {
1321
+ const positions = [];
1322
+ for (const cId of conditions) {
1323
+ const position = this.submitted.get(cId);
1324
+ if (position) {
1325
+ positions.push(position);
1326
+ } else {
1327
+ logger.error("no submitted failed position", cId);
1328
+ telegramService.error(`no submitted failed position: ${cId}`);
1329
+ }
1330
+ }
1331
+ return positions;
1118
1332
  }
1119
1333
  /**
1120
1334
  * 获取可 Redeem 的仓位
@@ -1151,145 +1365,63 @@ class Redeemer {
1151
1365
  }
1152
1366
  return positions;
1153
1367
  }
1154
- testRequestLimit(errorMsg) {
1155
- if (errorMsg.includes("429") || errorMsg.includes("Too Many Requests")) {
1156
- const match = errorMsg.match(/resets in (\d+) seconds/);
1157
- return match ? parseInt(match[1]) : 0;
1158
- }
1159
- return 0;
1160
- }
1161
- toMessages(records, withExecInfo) {
1162
- return records.map(
1163
- ({ position, matchedCount, failedCount }) => ` - ${position.slug} ${position.outcome}: ${position.currentValue} USDC${withExecInfo ? ` (成功匹配: ${matchedCount} 失败重试: ${failedCount})` : ""}`
1368
+ toMessages(positions) {
1369
+ return positions.map(
1370
+ (position) => ` - ${position.slug} ${position.outcome}: ${position.currentValue} USDC`
1164
1371
  );
1165
1372
  }
1166
- /**
1167
- * 执行一轮 Redeem
1168
- */
1169
1373
  async autoRedeem() {
1170
1374
  let redeemablePositions;
1171
1375
  try {
1172
1376
  redeemablePositions = await this.fetchRedeemablePositions();
1173
1377
  } catch (error) {
1174
- const errorMsg = error instanceof Error ? error.message : String(error);
1175
- logger.error(`自动 redeem: 获取仓位失败: ${errorMsg}`);
1378
+ logger.error("自动 redeem: 获取仓位失败", error);
1176
1379
  return;
1177
1380
  }
1178
- let requestLimit = 0;
1179
- const pendingRedeems = [];
1180
- const failedRedeems = [];
1181
- const successRedeem = [];
1182
1381
  for (const position of redeemablePositions) {
1183
- let record = this.records.get(position.conditionId);
1184
- if (!record) {
1185
- record = {
1186
- position,
1187
- success: false,
1188
- failedCount: 0,
1189
- matchedCount: 0
1190
- };
1191
- this.records.set(position.conditionId, record);
1192
- }
1193
- record.position = position;
1194
- if (record.success) {
1195
- record.matchedCount++;
1196
- if (record.matchedCount === MAX_MATCH_TIMES) {
1197
- failedRedeems.push(record);
1198
- }
1199
- } else if (record.failedCount < MAX_MATCH_TIMES) {
1200
- const insertIndex = lodash.sortedIndexBy(
1201
- pendingRedeems,
1202
- record,
1203
- "failedCount"
1204
- );
1205
- pendingRedeems.splice(insertIndex, 0, record);
1206
- }
1207
- }
1208
- const execRedeems = pendingRedeems.splice(0, MAX_BATCH_SIZE);
1209
- if (execRedeems.length > 0) {
1210
- try {
1211
- const txHash = await this.options.redeemFn(
1212
- execRedeems.map(({ position }) => position.conditionId)
1213
- );
1214
- logger.info(`Redeem 已执行 (批量 ${execRedeems.length}): ${txHash}`);
1215
- for (const record of execRedeems) record.success = true;
1216
- } catch (error) {
1217
- const errorMsg = error instanceof Error ? error.message : String(error);
1218
- logger.error(
1219
- `Redeem 批量异常 (批量 ${execRedeems.length}): ${errorMsg}`
1220
- );
1221
- requestLimit = this.testRequestLimit(errorMsg);
1222
- if (requestLimit <= 0) {
1223
- for (const record of execRedeems) {
1224
- record.failedCount++;
1225
- if (record.failedCount >= MAX_MATCH_TIMES) {
1226
- failedRedeems.push(record);
1227
- }
1382
+ const conditionId = position.conditionId;
1383
+ if (!this.submitted.has(conditionId)) {
1384
+ const relayClient = this.options.relayClient;
1385
+ relayClient.submitRedeem(conditionId).catch((error) => {
1386
+ if (relayClient.isSubmitError(error)) {
1387
+ logger.error("submitRedeem failed:", conditionId, error);
1388
+ telegramService.error(`submitRedeem failed: ${conditionId}`);
1228
1389
  }
1229
- }
1390
+ });
1230
1391
  }
1392
+ this.submitted.set(conditionId, position);
1231
1393
  }
1232
- for (const [conditionId, record] of this.records.entries()) {
1394
+ const successPositions = [];
1395
+ for (const [conditionId, position] of this.submitted.entries()) {
1233
1396
  if (!redeemablePositions.some((p) => p.conditionId === conditionId)) {
1234
- successRedeem.push(record);
1235
- this.records.delete(conditionId);
1397
+ successPositions.push(position);
1398
+ this.submitted.delete(conditionId);
1236
1399
  }
1237
1400
  }
1238
- const messages = [];
1239
- if (successRedeem.length > 0) {
1240
- messages.push(
1241
- "🟢 redeem 成功:",
1242
- ...this.toMessages(successRedeem, false)
1243
- );
1244
- }
1245
- if (failedRedeems.length > 0) {
1246
- messages.push(
1247
- "🔴 redeem 失败:",
1248
- ...this.toMessages(failedRedeems, false)
1249
- );
1250
- }
1251
- if (messages.length > 0) {
1401
+ if (successPositions.length > 0) {
1402
+ const messages = [
1403
+ `🟢 redeem 成功: ${successPositions.length} 个`,
1404
+ ...this.toMessages(successPositions)
1405
+ ];
1406
+ logger.lines(...messages);
1252
1407
  telegramService.info(messages.join("\n"));
1253
1408
  }
1254
- const logLines = [];
1255
- if (execRedeems.length > 0) {
1256
- logLines.push(
1257
- `redeem 本轮执行(待确认): ${execRedeems.length} 个`,
1258
- ...this.toMessages(execRedeems),
1259
- ""
1260
- );
1261
- }
1262
- if (pendingRedeems.length > 0) {
1263
- logLines.push(
1264
- `redeem 本轮未执行(等待下轮): ${pendingRedeems.length} 个`,
1265
- ...this.toMessages(pendingRedeems),
1266
- ""
1267
- );
1268
- }
1269
- if (successRedeem.length > 0) {
1270
- logLines.push(
1271
- `🟢 redeem 成功确认: ${successRedeem.length} 个`,
1272
- ...this.toMessages(successRedeem),
1273
- ""
1274
- );
1275
- }
1276
- if (failedRedeems.length > 0) {
1277
- logLines.push(
1278
- `🔴 redeem 失败已放弃(重试超限): ${failedRedeems.length} 个`,
1279
- ...this.toMessages(failedRedeems),
1280
- ""
1281
- );
1282
- }
1283
- logLines.push(`redeem 待确认总数: ${this.records.size} 个`);
1284
- for (const log of logLines) logger.info(log);
1285
- if (successRedeem.length > 0) {
1286
- this.options.onRedeemSuccess?.();
1287
- }
1288
- if (requestLimit > 0) {
1289
- logger.warning(`Redeem 限流,${requestLimit} 秒后重试`);
1290
- telegramService.warning("Redeem 限流", `${requestLimit} 秒后重试`);
1291
- this.restart(requestLimit * 1e3);
1292
- }
1409
+ logger.info(`redeem 待确认总数: ${this.submitted.size} 个`);
1410
+ if (successPositions.length > 0) this.emit("redeemed");
1411
+ }
1412
+ start() {
1413
+ if (this.running) return;
1414
+ this.running = true;
1415
+ this.timer = setPromiseInterval__default.default(
1416
+ this.autoRedeem.bind(this),
1417
+ AUTO_REDEEM_INTERVAL
1418
+ );
1419
+ }
1420
+ stop() {
1421
+ setPromiseInterval.clearPromiseInterval(this.timer);
1422
+ this.unListenRelayClient();
1423
+ this.running = false;
1424
+ logger.info("自动 redeem 已停止");
1293
1425
  }
1294
1426
  }
1295
1427
  const POSITION_POLL_INTERVAL = 5e3;
@@ -1305,7 +1437,6 @@ class Trader {
1305
1437
  initialized = false;
1306
1438
  constructor() {
1307
1439
  this.client = new ClobClientWrapper();
1308
- this.relayClient = new RelayClientWrapper();
1309
1440
  this.assetFilter = new AssetFilter();
1310
1441
  this.balanceCache = new BalanceCache();
1311
1442
  this.orderWatcher = new OrderWatcher();
@@ -1328,7 +1459,7 @@ class Trader {
1328
1459
  builderCreds: config.polymarket.builderCreds
1329
1460
  });
1330
1461
  if (config.polymarket.builderCreds) {
1331
- this.relayClient.init({
1462
+ this.relayClient = initRelayClient({
1332
1463
  privateKey: config.polymarket.privateKey,
1333
1464
  builderCreds: config.polymarket.builderCreds
1334
1465
  });
@@ -1345,13 +1476,13 @@ class Trader {
1345
1476
  if (!this.config?.polymarket?.builderCreds) {
1346
1477
  return;
1347
1478
  }
1348
- if (!this.redeemer) {
1479
+ if (!this.redeemer && this.relayClient) {
1349
1480
  this.redeemer = new Redeemer({
1350
1481
  funderAddress: this.config.polymarket.funderAddress,
1351
- redeemFn: this.executeRedeemBatch.bind(this),
1352
- onRedeemSuccess: () => {
1353
- this.balanceCache.invalidate();
1354
- }
1482
+ relayClient: this.relayClient
1483
+ });
1484
+ this.redeemer.addListener("redeemed", () => {
1485
+ this.balanceCache.invalidate();
1355
1486
  });
1356
1487
  this.redeemer.start();
1357
1488
  }
@@ -1533,7 +1664,7 @@ class Trader {
1533
1664
  * 通知需要 Redeem 兜底
1534
1665
  */
1535
1666
  notifyRedeemFallback(assetId, expectedSize) {
1536
- if (!this.relayClient.isInitialized()) {
1667
+ if (!this.relayClient) {
1537
1668
  const msg = "仓位同步超时且 Redeem 不可用";
1538
1669
  logger.warning(msg);
1539
1670
  telegramService.warning(
@@ -1563,19 +1694,11 @@ class Trader {
1563
1694
  telegramService.error("卖单提交失败", `错误: ${sellResult.errorMsg}`);
1564
1695
  }
1565
1696
  }
1566
- /**
1567
- * 批量执行 Redeem(供 Redeemer 调用)
1568
- */
1569
- async executeRedeemBatch(conditionIds) {
1570
- if (!this.relayClient.isInitialized()) {
1571
- throw new Error("RelayClient 未初始化");
1572
- }
1573
- return this.relayClient.redeemBatch(conditionIds);
1574
- }
1575
1697
  /**
1576
1698
  * 停止交易模块
1577
1699
  */
1578
1700
  async shutdown() {
1701
+ this.relayClient?.clear();
1579
1702
  this.redeemer?.stop();
1580
1703
  logger.info("交易模块已停止");
1581
1704
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polycopy",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "polycopy test",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "@polymarket/clob-client": "^5.2.0",
21
21
  "fs-extra": "^11.3.3",
22
22
  "grammy": "^1.39.2",
23
- "lodash": "^4.17.21",
23
+ "lodash": "^4.17.23",
24
24
  "set-promise-interval": "^1.1.0",
25
25
  "viem": "^2.44.2",
26
26
  "log4js": "^6.9.1"