hyperquant 1.41__tar.gz → 1.43__tar.gz
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.
- {hyperquant-1.41 → hyperquant-1.43}/PKG-INFO +1 -1
- {hyperquant-1.41 → hyperquant-1.43}/pyproject.toml +1 -1
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/models/polymarket.py +41 -33
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/polymarket.py +402 -9
- {hyperquant-1.41 → hyperquant-1.43}/uv.lock +1 -1
- {hyperquant-1.41 → hyperquant-1.43}/.gitignore +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/README.md +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/requirements-dev.lock +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/requirements.lock +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/__init__.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/auth.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/bitget.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/bitmart.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/coinw.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/deepcoin.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/edgex.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/hyperliquid.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/lbank.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/lib/hpstore.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/lib/hyper_types.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/lib/util.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/lighter.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/models/apexpro.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/models/bitget.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/models/bitmart.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/models/coinw.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/models/deepcoin.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/models/edgex.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/models/hyperliquid.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/models/lbank.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/models/lighter.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/models/ourbit.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/ourbit.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/broker/ws.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/core.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/datavison/_util.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/datavison/binance.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/datavison/coinglass.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/datavison/okx.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/db.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/draw.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/logkit.py +0 -0
- {hyperquant-1.41 → hyperquant-1.43}/src/hyperquant/notikit.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hyperquant
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.43
|
|
4
4
|
Summary: A minimal yet hyper-efficient backtesting framework for quantitative trading
|
|
5
5
|
Project-URL: Homepage, https://github.com/yourusername/hyperquant
|
|
6
6
|
Project-URL: Issues, https://github.com/yourusername/hyperquant/issues
|
|
@@ -225,17 +225,22 @@ class MyTrade(DataStore):
|
|
|
225
225
|
class Trade(DataStore):
|
|
226
226
|
"""User trades keyed by trade id."""
|
|
227
227
|
|
|
228
|
-
_KEYS = ["
|
|
228
|
+
_KEYS = ["hash"]
|
|
229
229
|
_MAXLEN = 500
|
|
230
230
|
|
|
231
|
-
|
|
232
231
|
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
233
232
|
payload = msg or {}
|
|
234
233
|
if payload:
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
234
|
+
if payload.get("event_type") == "last_trade_price":
|
|
235
|
+
transaction_hash = payload.get("transaction_hash")
|
|
236
|
+
if transaction_hash:
|
|
237
|
+
payload.update({"hash": transaction_hash})
|
|
238
|
+
payload.pop("transaction_hash", None)
|
|
239
|
+
else:
|
|
240
|
+
if payload.get("transactionHash"):
|
|
241
|
+
payload.update({"hash": payload.get("transactionHash")})
|
|
242
|
+
payload.pop("transactionHash", None)
|
|
243
|
+
|
|
239
244
|
self._insert([payload])
|
|
240
245
|
|
|
241
246
|
class Book(DataStore):
|
|
@@ -861,33 +866,25 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
861
866
|
_key asset
|
|
862
867
|
MATCHED进行快速捕捉
|
|
863
868
|
.. code:: json
|
|
864
|
-
|
|
865
|
-
"
|
|
866
|
-
"
|
|
867
|
-
"
|
|
868
|
-
"
|
|
869
|
-
"
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
}
|
|
878
|
-
],
|
|
879
|
-
"market": "0xbd31dc8a20211944f6b70f31557f1001557b59905b7738480ca09bd4532f84af",
|
|
880
|
-
"matchtime": "1672290701",
|
|
881
|
-
"outcome": "YES",
|
|
882
|
-
"owner": "9180014b-33c8-9240-a14b-bdca11c0a465",
|
|
883
|
-
"price": "0.57",
|
|
869
|
+
{
|
|
870
|
+
"asset": "12819879685513143002408869746992985182419696851931617234615358342350852997413",
|
|
871
|
+
"bio": "",
|
|
872
|
+
"conditionId": "0xea609d2c6bc2cb20e328be7c89f258b84b35bbe119b44e0a2cfc5f15e6642b3b",
|
|
873
|
+
"eventSlug": "btc-updown-15m-1763865000",
|
|
874
|
+
"icon": "https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png",
|
|
875
|
+
"name": "infusion",
|
|
876
|
+
"outcome": "Up",
|
|
877
|
+
"outcomeIndex": 0,
|
|
878
|
+
"price": 0.7,
|
|
879
|
+
"profileImage": "",
|
|
880
|
+
"proxyWallet": "0x2C060830B6F6B43174b1Cf8B4475db07703c1543",
|
|
881
|
+
"pseudonym": "Frizzy-Graduate",
|
|
884
882
|
"side": "BUY",
|
|
885
|
-
"size":
|
|
886
|
-
"
|
|
887
|
-
"
|
|
888
|
-
"
|
|
889
|
-
"
|
|
890
|
-
"type": "TRADE"
|
|
883
|
+
"size": 5,
|
|
884
|
+
"slug": "btc-updown-15m-1763865000",
|
|
885
|
+
"timestamp": 1763865085,
|
|
886
|
+
"title": "Bitcoin Up or Down - November 22, 9:30PM-9:45PM ET",
|
|
887
|
+
"hash": "0xddea11d695e811686f83379d9269accf1be581fbcb542809c6c67a3cc3002488"
|
|
891
888
|
}
|
|
892
889
|
"""
|
|
893
890
|
return self._get("trade")
|
|
@@ -912,7 +909,7 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
912
909
|
elif msg_type == "order":
|
|
913
910
|
self.orders._on_message(m)
|
|
914
911
|
elif msg_type == "trade":
|
|
915
|
-
self.
|
|
912
|
+
self.mytrade._on_message(m)
|
|
916
913
|
self.fill._on_trade(m)
|
|
917
914
|
self.position.on_trade(m)
|
|
918
915
|
elif msg_type == 'orders_matched':
|
|
@@ -934,3 +931,14 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
934
931
|
msg_type = str(raw_type).lower()
|
|
935
932
|
if msg_type in {"book", "price_change"}:
|
|
936
933
|
self.bbo._on_message(m)
|
|
934
|
+
|
|
935
|
+
def onmessage_for_last_trade(self, msg, ws = None):
|
|
936
|
+
# 判定msg是否为list
|
|
937
|
+
lst_msg = msg if isinstance(msg, list) else [msg]
|
|
938
|
+
for m in lst_msg:
|
|
939
|
+
raw_type = m.get("event_type") or m.get("type")
|
|
940
|
+
if not raw_type:
|
|
941
|
+
continue
|
|
942
|
+
msg_type = str(raw_type).lower()
|
|
943
|
+
if msg_type == "last_trade_price":
|
|
944
|
+
self.trade._on_message(m)
|
|
@@ -35,11 +35,86 @@ ERC20_BALANCE_OF_ABI = (
|
|
|
35
35
|
"\"name\":\"balanceOf\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],"
|
|
36
36
|
"\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"}]"
|
|
37
37
|
)
|
|
38
|
+
CONDITIONAL_TOKENS_ABI = [
|
|
39
|
+
{
|
|
40
|
+
"inputs": [
|
|
41
|
+
{
|
|
42
|
+
"internalType": "contract IERC20",
|
|
43
|
+
"name": "collateralToken",
|
|
44
|
+
"type": "address",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"internalType": "bytes32",
|
|
48
|
+
"name": "parentCollectionId",
|
|
49
|
+
"type": "bytes32",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"internalType": "bytes32",
|
|
53
|
+
"name": "conditionId",
|
|
54
|
+
"type": "bytes32",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"internalType": "uint256[]",
|
|
58
|
+
"name": "indexSets",
|
|
59
|
+
"type": "uint256[]",
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
"name": "redeemPositions",
|
|
63
|
+
"outputs": [],
|
|
64
|
+
"stateMutability": "nonpayable",
|
|
65
|
+
"type": "function",
|
|
66
|
+
}
|
|
67
|
+
]
|
|
38
68
|
DEFAULT_POLYGON_RPCS = (
|
|
39
69
|
"https://polygon.llamarpc.com",
|
|
40
70
|
"https://polygon-rpc.com",
|
|
41
71
|
"https://rpc.ankr.com/polygon",
|
|
42
72
|
)
|
|
73
|
+
SAFE_ABI = [
|
|
74
|
+
{
|
|
75
|
+
"inputs": [],
|
|
76
|
+
"name": "nonce",
|
|
77
|
+
"outputs": [{"internalType": "uint256", "name": "nonce", "type": "uint256"}],
|
|
78
|
+
"stateMutability": "view",
|
|
79
|
+
"type": "function",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"inputs": [
|
|
83
|
+
{"internalType": "address", "name": "to", "type": "address"},
|
|
84
|
+
{"internalType": "uint256", "name": "value", "type": "uint256"},
|
|
85
|
+
{"internalType": "bytes", "name": "data", "type": "bytes"},
|
|
86
|
+
{"internalType": "uint8", "name": "operation", "type": "uint8"},
|
|
87
|
+
{"internalType": "uint256", "name": "safeTxGas", "type": "uint256"},
|
|
88
|
+
{"internalType": "uint256", "name": "baseGas", "type": "uint256"},
|
|
89
|
+
{"internalType": "uint256", "name": "gasPrice", "type": "uint256"},
|
|
90
|
+
{"internalType": "address", "name": "gasToken", "type": "address"},
|
|
91
|
+
{"internalType": "address", "name": "refundReceiver", "type": "address"},
|
|
92
|
+
{"internalType": "uint256", "name": "_nonce", "type": "uint256"},
|
|
93
|
+
],
|
|
94
|
+
"name": "getTransactionHash",
|
|
95
|
+
"outputs": [{"internalType": "bytes32", "name": "txHash", "type": "bytes32"}],
|
|
96
|
+
"stateMutability": "view",
|
|
97
|
+
"type": "function",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"inputs": [
|
|
101
|
+
{"internalType": "address", "name": "to", "type": "address"},
|
|
102
|
+
{"internalType": "uint256", "name": "value", "type": "uint256"},
|
|
103
|
+
{"internalType": "bytes", "name": "data", "type": "bytes"},
|
|
104
|
+
{"internalType": "uint8", "name": "operation", "type": "uint8"},
|
|
105
|
+
{"internalType": "uint256", "name": "safeTxGas", "type": "uint256"},
|
|
106
|
+
{"internalType": "uint256", "name": "baseGas", "type": "uint256"},
|
|
107
|
+
{"internalType": "uint256", "name": "gasPrice", "type": "uint256"},
|
|
108
|
+
{"internalType": "address", "name": "gasToken", "type": "address"},
|
|
109
|
+
{"internalType": "address", "name": "refundReceiver", "type": "address"},
|
|
110
|
+
{"internalType": "bytes", "name": "signatures", "type": "bytes"},
|
|
111
|
+
],
|
|
112
|
+
"name": "execTransaction",
|
|
113
|
+
"outputs": [{"internalType": "bool", "name": "success", "type": "bool"}],
|
|
114
|
+
"stateMutability": "payable",
|
|
115
|
+
"type": "function",
|
|
116
|
+
},
|
|
117
|
+
]
|
|
43
118
|
_EASTERN_TZ = pytz.timezone("US/Eastern")
|
|
44
119
|
|
|
45
120
|
def parse_field(value):
|
|
@@ -333,6 +408,7 @@ class Polymarket:
|
|
|
333
408
|
token_ids: Sequence[str] | str,
|
|
334
409
|
wsapp: pybotters.ws.WebSocketApp | None = None,
|
|
335
410
|
only_bbo: bool = False,
|
|
411
|
+
with_trades: bool = False
|
|
336
412
|
) -> pybotters.ws.WebSocketApp:
|
|
337
413
|
"""Subscribe to public order-book updates for the provided token ids."""
|
|
338
414
|
|
|
@@ -341,11 +417,14 @@ class Polymarket:
|
|
|
341
417
|
if wsapp:
|
|
342
418
|
await wsapp.current_ws.send_json(payload)
|
|
343
419
|
hdrl_json = self.store.onmessage_for_bbo if only_bbo else self.store.onmessage
|
|
420
|
+
hd_lst = [hdrl_json]
|
|
421
|
+
if with_trades:
|
|
422
|
+
hd_lst.append(self.store.onmessage_for_last_trade)
|
|
344
423
|
|
|
345
424
|
self._ws_public = self.client.ws_connect(
|
|
346
425
|
self.ws_public,
|
|
347
426
|
send_json=payload,
|
|
348
|
-
hdlr_json=
|
|
427
|
+
hdlr_json=hd_lst
|
|
349
428
|
)
|
|
350
429
|
await self._ws_public._event.wait()
|
|
351
430
|
return self._ws_public
|
|
@@ -388,7 +467,7 @@ class Polymarket:
|
|
|
388
467
|
return self._ws_personal
|
|
389
468
|
|
|
390
469
|
async def sub_trades(self, slug: str):
|
|
391
|
-
|
|
470
|
+
"""订阅activate trades"""
|
|
392
471
|
payload = {
|
|
393
472
|
"action": "subscribe",
|
|
394
473
|
"subscriptions": [
|
|
@@ -420,7 +499,10 @@ class Polymarket:
|
|
|
420
499
|
await wsapp._event.wait()
|
|
421
500
|
await wsapp.current_ws.send_json(payload)
|
|
422
501
|
return wsapp
|
|
423
|
-
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
|
|
424
506
|
# ------------------------------------------------------------------
|
|
425
507
|
# Public REST endpoints
|
|
426
508
|
|
|
@@ -1239,6 +1321,112 @@ class Polymarket:
|
|
|
1239
1321
|
position = position / 1e6
|
|
1240
1322
|
return position
|
|
1241
1323
|
|
|
1324
|
+
async def claim_positions(
|
|
1325
|
+
self,
|
|
1326
|
+
*,
|
|
1327
|
+
user: str | None = None,
|
|
1328
|
+
dry_run: bool = False,
|
|
1329
|
+
rpc_url: str | None = None,
|
|
1330
|
+
rpc_urls: Sequence[str] | None = None,
|
|
1331
|
+
gas: int = 300000,
|
|
1332
|
+
verbose: bool = False,
|
|
1333
|
+
limit: int = 500,
|
|
1334
|
+
include_receipt: bool = False,
|
|
1335
|
+
) -> list[dict[str, Any]]:
|
|
1336
|
+
"""自动拉取 data-api 的 redeemable 头寸并逐个 claim。
|
|
1337
|
+
|
|
1338
|
+
只对满足 redeemable==True 且 curPrice==1 且 size>0 的仓位执行,index_set=1<<outcomeIndex。
|
|
1339
|
+
始终使用 Safe 方式执行 redeemPositions(owner 私钥签名)。
|
|
1340
|
+
"""
|
|
1341
|
+
# 默认用代理钱包/资金钱包
|
|
1342
|
+
if user is None:
|
|
1343
|
+
entry = self._api_entry()
|
|
1344
|
+
user = entry[2] if entry and len(entry) > 2 else None
|
|
1345
|
+
if not user:
|
|
1346
|
+
raise RuntimeError("Polymarket claim_positions 需要提供 user (proxy wallet)")
|
|
1347
|
+
if not self.funder:
|
|
1348
|
+
self.funder = user
|
|
1349
|
+
|
|
1350
|
+
# 构造候选 RPC 列表 (单个 rpc_url 优先, 其次用户传入 rpc_urls, 最后内置默认)
|
|
1351
|
+
candidates: list[str] = []
|
|
1352
|
+
if rpc_url:
|
|
1353
|
+
candidates.append(rpc_url)
|
|
1354
|
+
for u in (rpc_urls or []):
|
|
1355
|
+
if u and u not in candidates:
|
|
1356
|
+
candidates.append(u)
|
|
1357
|
+
for u in DEFAULT_POLYGON_RPCS:
|
|
1358
|
+
if u not in candidates:
|
|
1359
|
+
candidates.append(u)
|
|
1360
|
+
|
|
1361
|
+
params = {"user": user, "limit": limit}
|
|
1362
|
+
positions = await self._rest(
|
|
1363
|
+
"GET",
|
|
1364
|
+
"/positions",
|
|
1365
|
+
params=params,
|
|
1366
|
+
host=DEFAULT_DATA_ENDPOINT,
|
|
1367
|
+
)
|
|
1368
|
+
|
|
1369
|
+
claimable: list[dict[str, Any]] = []
|
|
1370
|
+
for pos in positions or []:
|
|
1371
|
+
try:
|
|
1372
|
+
size = float(pos.get("size", 0) or 0)
|
|
1373
|
+
except Exception:
|
|
1374
|
+
size = 0.0
|
|
1375
|
+
redeemable = bool(pos.get("redeemable"))
|
|
1376
|
+
try:
|
|
1377
|
+
cur_price = float(pos.get("curPrice", 0) or 0)
|
|
1378
|
+
except Exception:
|
|
1379
|
+
cur_price = 0.0
|
|
1380
|
+
|
|
1381
|
+
condition_id = pos.get("conditionId")
|
|
1382
|
+
outcome_idx = pos.get("outcomeIndex") or pos.get("outcome_index") or 0
|
|
1383
|
+
if (
|
|
1384
|
+
not condition_id
|
|
1385
|
+
or not redeemable
|
|
1386
|
+
or size <= 0
|
|
1387
|
+
or cur_price != 1
|
|
1388
|
+
):
|
|
1389
|
+
continue
|
|
1390
|
+
try:
|
|
1391
|
+
idx_set = 1 << int(outcome_idx)
|
|
1392
|
+
except Exception:
|
|
1393
|
+
idx_set = 1
|
|
1394
|
+
claimable.append(
|
|
1395
|
+
{
|
|
1396
|
+
"condition_id": condition_id,
|
|
1397
|
+
"index_sets": [idx_set],
|
|
1398
|
+
"size": size,
|
|
1399
|
+
"outcome": pos.get("outcome"),
|
|
1400
|
+
"title": pos.get("title"),
|
|
1401
|
+
}
|
|
1402
|
+
)
|
|
1403
|
+
|
|
1404
|
+
results: list[dict[str, Any]] = []
|
|
1405
|
+
for item in claimable:
|
|
1406
|
+
if dry_run:
|
|
1407
|
+
results.append({**item, "tx": None, "dry_run": True})
|
|
1408
|
+
continue
|
|
1409
|
+
tx_hash = await asyncio.to_thread(
|
|
1410
|
+
self._claim_via_safe_sync,
|
|
1411
|
+
candidates,
|
|
1412
|
+
item["condition_id"],
|
|
1413
|
+
item["index_sets"],
|
|
1414
|
+
gas,
|
|
1415
|
+
verbose,
|
|
1416
|
+
)
|
|
1417
|
+
result_row = {**item, "tx": tx_hash, "dry_run": False}
|
|
1418
|
+
if include_receipt:
|
|
1419
|
+
try:
|
|
1420
|
+
receipt_info = await self.decode_claim_receipt(
|
|
1421
|
+
tx_hash,
|
|
1422
|
+
rpc_url=rpc_url or (rpc_urls[0] if rpc_urls else None),
|
|
1423
|
+
)
|
|
1424
|
+
result_row.update({"receipt": receipt_info})
|
|
1425
|
+
except Exception as exc: # pragma: no cover - 辅助信息获取失败
|
|
1426
|
+
result_row.update({"receipt_error": str(exc)})
|
|
1427
|
+
results.append(result_row)
|
|
1428
|
+
return results
|
|
1429
|
+
|
|
1242
1430
|
async def get_usdc_web3(
|
|
1243
1431
|
self,
|
|
1244
1432
|
wallet: str = None,
|
|
@@ -1279,6 +1467,189 @@ class Polymarket:
|
|
|
1279
1467
|
# ------------------------------------------------------------------
|
|
1280
1468
|
# Internal utilities
|
|
1281
1469
|
|
|
1470
|
+
async def decode_claim_receipt(
|
|
1471
|
+
self,
|
|
1472
|
+
tx_hash: str,
|
|
1473
|
+
*,
|
|
1474
|
+
rpc_url: str | None = None,
|
|
1475
|
+
) -> dict[str, Any]:
|
|
1476
|
+
if not tx_hash:
|
|
1477
|
+
raise ValueError("tx_hash is required")
|
|
1478
|
+
url = rpc_url or DEFAULT_POLYGON_RPCS[0]
|
|
1479
|
+
return await asyncio.to_thread(self._decode_payout_receipt_sync, tx_hash, url)
|
|
1480
|
+
|
|
1481
|
+
def _decode_payout_receipt_sync(self, tx_hash: str, rpc_url: str) -> dict[str, Any]:
|
|
1482
|
+
w3 = _get_web3(rpc_url)
|
|
1483
|
+
receipt = w3.eth.get_transaction_receipt(tx_hash)
|
|
1484
|
+
contract_cfg = self._contracts(self.chain_id, False)
|
|
1485
|
+
ctf_addr = w3.to_checksum_address(contract_cfg["conditional_tokens"])
|
|
1486
|
+
ctf = w3.eth.contract(address=ctf_addr, abi=CONDITIONAL_TOKENS_ABI)
|
|
1487
|
+
|
|
1488
|
+
decoded = []
|
|
1489
|
+
try:
|
|
1490
|
+
decoded = ctf.events.PayoutRedemption().process_receipt(receipt)
|
|
1491
|
+
except Exception:
|
|
1492
|
+
decoded = []
|
|
1493
|
+
|
|
1494
|
+
if decoded:
|
|
1495
|
+
ev = decoded[0]["args"]
|
|
1496
|
+
index_sets = [int(x) for x in ev.get("indexSets", [])]
|
|
1497
|
+
payout = int(ev.get("payout", 0))
|
|
1498
|
+
return {
|
|
1499
|
+
"status": receipt.status,
|
|
1500
|
+
"gasUsed": receipt.gasUsed,
|
|
1501
|
+
"indexSets": index_sets,
|
|
1502
|
+
"payout": payout,
|
|
1503
|
+
"redeemer": ev.get("redeemer"),
|
|
1504
|
+
"collateralToken": ev.get("collateralToken"),
|
|
1505
|
+
"conditionId": ev.get("conditionId").hex()
|
|
1506
|
+
if hasattr(ev.get("conditionId"), "hex")
|
|
1507
|
+
else ev.get("conditionId"),
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
return {"status": receipt.status, "gasUsed": receipt.gasUsed}
|
|
1511
|
+
|
|
1512
|
+
def _claim_via_safe_sync(
|
|
1513
|
+
self,
|
|
1514
|
+
rpc_urls: Sequence[str],
|
|
1515
|
+
condition_id: str,
|
|
1516
|
+
index_sets: list[int],
|
|
1517
|
+
gas: int,
|
|
1518
|
+
verbose: bool = False,
|
|
1519
|
+
) -> str:
|
|
1520
|
+
from eth_account import Account as _A
|
|
1521
|
+
from time import sleep
|
|
1522
|
+
|
|
1523
|
+
private_key, _, signer_addr = self._get_signing_context()
|
|
1524
|
+
safe_addr = self.funder
|
|
1525
|
+
if not safe_addr:
|
|
1526
|
+
raise RuntimeError("Safe/proxy wallet address未知, 请在 apis['polymarket'][2] 或构造函数 funder 设置")
|
|
1527
|
+
|
|
1528
|
+
w3 = None
|
|
1529
|
+
last_error: Exception | None = None
|
|
1530
|
+
for url in rpc_urls:
|
|
1531
|
+
try:
|
|
1532
|
+
w3 = _get_web3(url)
|
|
1533
|
+
break
|
|
1534
|
+
except Exception as exc:
|
|
1535
|
+
last_error = exc
|
|
1536
|
+
continue
|
|
1537
|
+
if w3 is None:
|
|
1538
|
+
raise RuntimeError(f"All RPC endpoints failed: {rpc_urls}") from last_error
|
|
1539
|
+
|
|
1540
|
+
ctf_addr = self._contracts(self.chain_id, False)["conditional_tokens"]
|
|
1541
|
+
ctf = w3.eth.contract(address=w3.to_checksum_address(ctf_addr), abi=CONDITIONAL_TOKENS_ABI)
|
|
1542
|
+
safe = w3.eth.contract(address=w3.to_checksum_address(safe_addr), abi=SAFE_ABI)
|
|
1543
|
+
|
|
1544
|
+
cond_bytes = bytes.fromhex(condition_id.replace("0x", ""))
|
|
1545
|
+
if len(cond_bytes) != 32:
|
|
1546
|
+
raise ValueError("condition_id must be 32-byte hex string")
|
|
1547
|
+
|
|
1548
|
+
redeem_calldata = ctf.encode_abi(
|
|
1549
|
+
"redeemPositions",
|
|
1550
|
+
args=[
|
|
1551
|
+
w3.to_checksum_address(USDC_CONTRACT),
|
|
1552
|
+
bytes(32),
|
|
1553
|
+
cond_bytes,
|
|
1554
|
+
index_sets,
|
|
1555
|
+
],
|
|
1556
|
+
)
|
|
1557
|
+
|
|
1558
|
+
safe_tx_gas = 0
|
|
1559
|
+
base_gas = 0
|
|
1560
|
+
gas_price = 0
|
|
1561
|
+
gas_token = ZERO_ADDRESS
|
|
1562
|
+
refund_receiver = ZERO_ADDRESS
|
|
1563
|
+
value = 0
|
|
1564
|
+
operation = 0 # CALL
|
|
1565
|
+
|
|
1566
|
+
try:
|
|
1567
|
+
safe_nonce = safe.functions.nonce().call()
|
|
1568
|
+
except Exception as exc:
|
|
1569
|
+
raise RuntimeError("无法获取 Safe nonce, 请确认 funder 地址为有效 Safe") from exc
|
|
1570
|
+
|
|
1571
|
+
try:
|
|
1572
|
+
safe_tx_hash = safe.functions.getTransactionHash(
|
|
1573
|
+
w3.to_checksum_address(ctf_addr),
|
|
1574
|
+
value,
|
|
1575
|
+
redeem_calldata,
|
|
1576
|
+
operation,
|
|
1577
|
+
safe_tx_gas,
|
|
1578
|
+
base_gas,
|
|
1579
|
+
gas_price,
|
|
1580
|
+
w3.to_checksum_address(gas_token),
|
|
1581
|
+
w3.to_checksum_address(refund_receiver),
|
|
1582
|
+
safe_nonce,
|
|
1583
|
+
).call()
|
|
1584
|
+
except Exception as exc:
|
|
1585
|
+
raise RuntimeError("Safe getTransactionHash 调用失败") from exc
|
|
1586
|
+
|
|
1587
|
+
signed = _A._sign_hash(safe_tx_hash, private_key) # eth_sign 风格
|
|
1588
|
+
sig_bytes = (
|
|
1589
|
+
int(signed.r).to_bytes(32, "big")
|
|
1590
|
+
+ int(signed.s).to_bytes(32, "big")
|
|
1591
|
+
+ bytes([signed.v])
|
|
1592
|
+
)
|
|
1593
|
+
|
|
1594
|
+
try:
|
|
1595
|
+
acct = _A.from_key(private_key)
|
|
1596
|
+
sender = acct.address
|
|
1597
|
+
nonce = w3.eth.get_transaction_count(sender)
|
|
1598
|
+
gas_price_chain = w3.eth.gas_price
|
|
1599
|
+
except Exception as exc:
|
|
1600
|
+
raise RuntimeError("获取 sender nonce/gas_price 失败") from exc
|
|
1601
|
+
|
|
1602
|
+
tx = safe.functions.execTransaction(
|
|
1603
|
+
w3.to_checksum_address(ctf_addr),
|
|
1604
|
+
value,
|
|
1605
|
+
redeem_calldata,
|
|
1606
|
+
operation,
|
|
1607
|
+
safe_tx_gas,
|
|
1608
|
+
base_gas,
|
|
1609
|
+
gas_price,
|
|
1610
|
+
w3.to_checksum_address(gas_token),
|
|
1611
|
+
w3.to_checksum_address(refund_receiver),
|
|
1612
|
+
sig_bytes,
|
|
1613
|
+
).build_transaction(
|
|
1614
|
+
{
|
|
1615
|
+
"from": sender,
|
|
1616
|
+
"nonce": nonce,
|
|
1617
|
+
"gas": gas,
|
|
1618
|
+
"gasPrice": gas_price_chain,
|
|
1619
|
+
}
|
|
1620
|
+
)
|
|
1621
|
+
|
|
1622
|
+
signed_tx = w3.eth.account.sign_transaction(tx, private_key)
|
|
1623
|
+
send_errors: list[str] = []
|
|
1624
|
+
for attempt in range(3):
|
|
1625
|
+
try:
|
|
1626
|
+
raw_tx = getattr(signed_tx, "rawTransaction", None) or getattr(signed_tx, "raw_transaction", None)
|
|
1627
|
+
if raw_tx is None: # pragma: no cover
|
|
1628
|
+
raise AttributeError("Signed transaction missing rawTransaction/raw_transaction")
|
|
1629
|
+
tx_hash = w3.eth.send_raw_transaction(raw_tx)
|
|
1630
|
+
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
|
|
1631
|
+
if receipt.get("status") != 1:
|
|
1632
|
+
raise RuntimeError(f"Safe redeemPositions failed status!=1: {tx_hash.hex()}")
|
|
1633
|
+
if verbose:
|
|
1634
|
+
print(
|
|
1635
|
+
{
|
|
1636
|
+
"tx": tx_hash.hex(),
|
|
1637
|
+
"safe_nonce": safe_nonce,
|
|
1638
|
+
"wallet": sender,
|
|
1639
|
+
"gasPrice": gas_price_chain,
|
|
1640
|
+
"gas": gas,
|
|
1641
|
+
"rpc_used": getattr(w3.provider, "endpoint_uri", "unknown"),
|
|
1642
|
+
}
|
|
1643
|
+
)
|
|
1644
|
+
return tx_hash.hex()
|
|
1645
|
+
except Exception as exc:
|
|
1646
|
+
send_errors.append(str(exc))
|
|
1647
|
+
if verbose:
|
|
1648
|
+
print(f"Safe redeem attempt {attempt+1} failed: {exc}")
|
|
1649
|
+
sleep(0.5)
|
|
1650
|
+
continue
|
|
1651
|
+
raise RuntimeError(f"Safe redeem all attempts failed: {' | '.join(send_errors)}")
|
|
1652
|
+
|
|
1282
1653
|
async def _fetch_event(self, slug: str) -> dict | None:
|
|
1283
1654
|
resp = await self.client.get(GAMMA_EVENTS_API, params={"slug": slug})
|
|
1284
1655
|
payload = await resp.json()
|
|
@@ -1563,11 +1934,33 @@ class Polymarket:
|
|
|
1563
1934
|
return [pk, "", (api_key, api_secret, passphrase, chain_id, wallet_address)]
|
|
1564
1935
|
|
|
1565
1936
|
@lru_cache(maxsize=8)
|
|
1566
|
-
def _get_web3(rpc_url: str):
|
|
1937
|
+
def _get_web3(rpc_url: str | None):
|
|
1938
|
+
"""创建 web3 对象, 带多 RPC 备用与重试.
|
|
1939
|
+
|
|
1940
|
+
逻辑:
|
|
1941
|
+
1. 优先使用传入 rpc_url (如果提供)
|
|
1942
|
+
2. 失败则按 DEFAULT_POLYGON_RPCS 顺序依次尝试
|
|
1943
|
+
3. 任一连接成功立即返回, 全部失败抛出统一异常
|
|
1944
|
+
"""
|
|
1567
1945
|
from web3 import Web3
|
|
1568
1946
|
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1947
|
+
candidates: list[str] = []
|
|
1948
|
+
if rpc_url:
|
|
1949
|
+
candidates.append(rpc_url)
|
|
1950
|
+
for u in DEFAULT_POLYGON_RPCS:
|
|
1951
|
+
if u not in candidates:
|
|
1952
|
+
candidates.append(u)
|
|
1953
|
+
|
|
1954
|
+
last_error: Exception | None = None
|
|
1955
|
+
for url in candidates:
|
|
1956
|
+
try:
|
|
1957
|
+
provider = Web3.HTTPProvider(url, request_kwargs={"timeout": 7})
|
|
1958
|
+
w3 = Web3(provider)
|
|
1959
|
+
if w3.is_connected():
|
|
1960
|
+
return w3
|
|
1961
|
+
last_error = RuntimeError(f"RPC not connected: {url}")
|
|
1962
|
+
except Exception as exc: # pragma: no cover - 网络异常
|
|
1963
|
+
last_error = exc
|
|
1964
|
+
continue
|
|
1965
|
+
|
|
1966
|
+
raise RuntimeError(f"Failed to connect Polygon RPCs: {candidates}") from last_error
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|