polycopy 0.2.5 → 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.
- package/dist/index.js +372 -249
- 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(
|
|
80
|
-
log4jsLogger.info(
|
|
78
|
+
info(...messages) {
|
|
79
|
+
log4jsLogger.info(messages[0], ...messages.slice(1));
|
|
81
80
|
}
|
|
82
81
|
/**
|
|
83
|
-
*
|
|
82
|
+
* 输出成功日志
|
|
84
83
|
*/
|
|
85
|
-
success(
|
|
86
|
-
log4jsLogger.info(
|
|
84
|
+
success(...messages) {
|
|
85
|
+
log4jsLogger.info("✅", ...messages);
|
|
87
86
|
}
|
|
88
87
|
/**
|
|
89
|
-
*
|
|
88
|
+
* 输出警告日志
|
|
90
89
|
*/
|
|
91
|
-
warning(
|
|
92
|
-
log4jsLogger.warn(
|
|
90
|
+
warning(...messages) {
|
|
91
|
+
log4jsLogger.warn("⚠️", ...messages);
|
|
93
92
|
}
|
|
94
93
|
/**
|
|
95
|
-
*
|
|
94
|
+
* 输出错误日志
|
|
96
95
|
*/
|
|
97
|
-
error(
|
|
98
|
-
log4jsLogger.error(
|
|
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
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
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
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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.
|
|
878
|
-
|
|
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
|
-
|
|
884
|
-
|
|
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
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
|
|
921
|
-
|
|
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
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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 =
|
|
1084
|
-
|
|
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
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
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
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1184
|
-
if (!
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
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
|
-
|
|
1394
|
+
const successPositions = [];
|
|
1395
|
+
for (const [conditionId, position] of this.submitted.entries()) {
|
|
1233
1396
|
if (!redeemablePositions.some((p) => p.conditionId === conditionId)) {
|
|
1234
|
-
|
|
1235
|
-
this.
|
|
1397
|
+
successPositions.push(position);
|
|
1398
|
+
this.submitted.delete(conditionId);
|
|
1236
1399
|
}
|
|
1237
1400
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
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
|
-
|
|
1255
|
-
if (
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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
|
|
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
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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"
|