hyperquant 1.53__tar.gz → 1.55__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.
Files changed (46) hide show
  1. {hyperquant-1.53 → hyperquant-1.55}/PKG-INFO +1 -1
  2. {hyperquant-1.53 → hyperquant-1.55}/pyproject.toml +1 -1
  3. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/polymarket.py +401 -73
  4. {hyperquant-1.53 → hyperquant-1.55}/uv.lock +1 -1
  5. {hyperquant-1.53 → hyperquant-1.55}/.gitignore +0 -0
  6. {hyperquant-1.53 → hyperquant-1.55}/README.md +0 -0
  7. {hyperquant-1.53 → hyperquant-1.55}/requirements-dev.lock +0 -0
  8. {hyperquant-1.53 → hyperquant-1.55}/requirements.lock +0 -0
  9. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/__init__.py +0 -0
  10. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/auth.py +0 -0
  11. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/bitget.py +0 -0
  12. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/bitmart.py +0 -0
  13. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/coinw.py +0 -0
  14. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/deepcoin.py +0 -0
  15. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/edgex.py +0 -0
  16. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/hyperliquid.py +0 -0
  17. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/lbank.py +0 -0
  18. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
  19. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/lib/hpstore.py +0 -0
  20. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/lib/hyper_types.py +0 -0
  21. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/lib/polymarket/ctfAbi.py +0 -0
  22. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/lib/polymarket/safeAbi.py +0 -0
  23. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/lib/util.py +0 -0
  24. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/lighter.py +0 -0
  25. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/apexpro.py +0 -0
  26. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/bitget.py +0 -0
  27. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/bitmart.py +0 -0
  28. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/coinw.py +0 -0
  29. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/deepcoin.py +0 -0
  30. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/edgex.py +0 -0
  31. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/hyperliquid.py +0 -0
  32. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/lbank.py +0 -0
  33. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/lighter.py +0 -0
  34. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/ourbit.py +0 -0
  35. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/models/polymarket.py +0 -0
  36. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/ourbit.py +0 -0
  37. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/broker/ws.py +0 -0
  38. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/core.py +0 -0
  39. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/datavison/_util.py +0 -0
  40. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/datavison/binance.py +0 -0
  41. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/datavison/coinglass.py +0 -0
  42. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/datavison/okx.py +0 -0
  43. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/db.py +0 -0
  44. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/draw.py +0 -0
  45. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/logkit.py +0 -0
  46. {hyperquant-1.53 → hyperquant-1.55}/src/hyperquant/notikit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperquant
3
- Version: 1.53
3
+ Version: 1.55
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hyperquant"
3
- version = "1.53"
3
+ version = "1.55"
4
4
  description = "A minimal yet hyper-efficient backtesting framework for quantitative trading"
5
5
  authors = [
6
6
  { name = "MissinA", email = "1421329142@qq.com" }
@@ -7,7 +7,8 @@ from datetime import UTC, datetime, timedelta
7
7
  from functools import lru_cache
8
8
  import os
9
9
  import time
10
- from typing import Any, Iterable, Iterator, Literal, Mapping, Sequence
10
+ from typing import Any, Awaitable, Callable, Iterable, Iterator, Literal, Mapping, Sequence
11
+ from urllib.parse import urlsplit, urlunsplit
11
12
 
12
13
  import json
13
14
 
@@ -108,7 +109,10 @@ CONDITIONAL_TOKENS_SPLIT_ABI = [
108
109
  },
109
110
  ]
110
111
  DEFAULT_POLYGON_RPCS = (
111
- # "https://polygon.llamarpc.com",
112
+ "https://1rpc.io/matic",
113
+ "https://polygon-bor-rpc.publicnode.com",
114
+ "https://polygon.drpc.org",
115
+ # Keep historical endpoints as fallback for users with API access.
112
116
  "https://polygon-rpc.com",
113
117
  "https://rpc.ankr.com/polygon",
114
118
  )
@@ -256,6 +260,8 @@ class Polymarket:
256
260
  self._ws_personal: pybotters.ws.WebSocketApp | None = None
257
261
  self.auth = False
258
262
  self._tick_size_cache: dict[str, tuple[str, float]] = {}
263
+ self._forward_orders_cfg: dict[str, Any] | None = None
264
+ self._forward_ws: dict[str, Any] | None = None
259
265
 
260
266
  self._ensure_session_entry(private_key=private_key, funder=funder, chain_id=chain_id)
261
267
 
@@ -273,6 +279,7 @@ class Polymarket:
273
279
  await self._ws_public.current_ws.close()
274
280
  self._ws_public = None
275
281
  self._ws_public_ready.clear()
282
+ await self._close_forward_ws()
276
283
 
277
284
  async def prewarm_connections(
278
285
  self,
@@ -293,6 +300,109 @@ class Polymarket:
293
300
  except Exception:
294
301
  continue
295
302
 
303
+ async def mount_forward_orders(
304
+ self,
305
+ *,
306
+ mode: Literal["rest", "ws"],
307
+ url: str,
308
+ region: str = "afr",
309
+ session: str = "orders",
310
+ ws_timeout: float = 30.0,
311
+ batch_mode: Literal["step", "batch"] = "step",
312
+ concurrency: int = 5,
313
+ callback: Callable[[dict[str, Any]], Awaitable[None] | None] | None = None,
314
+ ) -> None:
315
+ """Mount forward execution for place_orders/cancel_orders."""
316
+ cfg = {
317
+ "mode": mode,
318
+ "url": url,
319
+ "region": region,
320
+ "session": session,
321
+ "ws_timeout": float(ws_timeout),
322
+ "batch_mode": batch_mode,
323
+ "concurrency": int(concurrency),
324
+ "callback": callback,
325
+ }
326
+ if mode == "rest":
327
+ cfg["url"] = self._normalize_forward_batch_url(url)
328
+ await self._close_forward_ws()
329
+ else:
330
+ cfg["url"] = self._normalize_forward_ws_url(url)
331
+ await self._ensure_forward_ws_connected(cfg)
332
+ self._forward_orders_cfg = cfg
333
+
334
+ async def unmount_forward_orders(self) -> None:
335
+ """Disable forward execution and fallback to direct CLOB calls."""
336
+ await self._close_forward_ws()
337
+ self._forward_orders_cfg = None
338
+
339
+ async def _close_forward_ws(self) -> None:
340
+ state = self._forward_ws
341
+ self._forward_ws = None
342
+ if not state:
343
+ return
344
+ task: asyncio.Task | None = state.get("listener")
345
+ ws: aiohttp.ClientWebSocketResponse | None = state.get("ws")
346
+ if task:
347
+ task.cancel()
348
+ try:
349
+ await task
350
+ except asyncio.CancelledError:
351
+ pass
352
+ except Exception:
353
+ pass
354
+ if ws and not ws.closed:
355
+ with suppress(Exception):
356
+ await ws.close()
357
+
358
+ async def _forward_ws_listener(self, ws: aiohttp.ClientWebSocketResponse, cfg: Mapping[str, Any]) -> None:
359
+ callback = cfg.get("callback")
360
+ while True:
361
+ msg = await ws.receive()
362
+ if msg.type == aiohttp.WSMsgType.TEXT:
363
+ try:
364
+ data = json.loads(msg.data)
365
+ except Exception:
366
+ continue
367
+ if callback:
368
+ try:
369
+ ret = callback(data)
370
+ if asyncio.iscoroutine(ret):
371
+ await ret
372
+ except Exception as exc:
373
+ self.logger.warning("forward ws callback error: %s", exc)
374
+ continue
375
+ if msg.type in {aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED}:
376
+ break
377
+ if msg.type == aiohttp.WSMsgType.ERROR:
378
+ break
379
+
380
+ async def _ensure_forward_ws_connected(self, cfg: Mapping[str, Any] | None = None) -> None:
381
+ current = cfg or self._forward_orders_cfg
382
+ if not current or current.get("mode") != "ws":
383
+ return
384
+ state = self._forward_ws
385
+ if state:
386
+ ws: aiohttp.ClientWebSocketResponse | None = state.get("ws")
387
+ task: asyncio.Task | None = state.get("listener")
388
+ if ws and not ws.closed and task and not task.done():
389
+ return
390
+ await self._close_forward_ws()
391
+
392
+ session: aiohttp.ClientSession = getattr(self.client, "_session", None)
393
+ if session is None:
394
+ raise RuntimeError("pybotters client session missing")
395
+
396
+ ws_url = current["url"]
397
+ sep = "&" if ("?" in ws_url) else "?"
398
+ ws_url = f"{ws_url}{sep}region={current.get('region', 'afr')}&session={current.get('session', 'orders')}"
399
+ ws = await session.ws_connect(ws_url, heartbeat=30)
400
+ # Consume hello once to align message stream.
401
+ with suppress(Exception):
402
+ await asyncio.wait_for(ws.receive(), timeout=float(current.get("ws_timeout", 30.0)))
403
+ listener = asyncio.create_task(self._forward_ws_listener(ws, current))
404
+ self._forward_ws = {"ws": ws, "listener": listener}
405
+
296
406
  # ------------------------------------------------------------------
297
407
  # Store helpers
298
408
 
@@ -1355,6 +1465,101 @@ class Polymarket:
1355
1465
  except Exception:
1356
1466
  return await resp.text()
1357
1467
 
1468
+ def _normalize_forward_batch_url(self, forward_url: str) -> str:
1469
+ parts = urlsplit(forward_url)
1470
+ if not parts.scheme or not parts.netloc:
1471
+ raise ValueError("forward_url must be a valid absolute URL")
1472
+ path = (parts.path or "").rstrip("/")
1473
+ if path.endswith("/forward-batch") or path.endswith("/forward-batch-step"):
1474
+ target_path = path
1475
+ elif path.endswith("/forward"):
1476
+ target_path = f"{path[:-len('/forward')]}/forward-batch-step"
1477
+ else:
1478
+ target_path = f"{path}/forward-batch-step" if path else "/forward-batch-step"
1479
+ return urlunsplit((parts.scheme, parts.netloc, target_path, parts.query, parts.fragment))
1480
+
1481
+ def _normalize_forward_ws_url(self, forward_url: str) -> str:
1482
+ parts = urlsplit(forward_url)
1483
+ if parts.scheme not in {"ws", "wss"} or not parts.netloc:
1484
+ raise ValueError("forward ws url must be a valid ws:// or wss:// URL")
1485
+ path = (parts.path or "").rstrip("/")
1486
+ if not path.endswith("/ws-forward"):
1487
+ path = f"{path}/ws-forward" if path else "/ws-forward"
1488
+ return urlunsplit((parts.scheme, parts.netloc, path, parts.query, parts.fragment))
1489
+
1490
+ def _build_l2_signed_headers(
1491
+ self,
1492
+ *,
1493
+ signer_addr: str,
1494
+ api_key: str,
1495
+ api_secret: str,
1496
+ api_passphrase: str,
1497
+ method: str,
1498
+ path: str,
1499
+ payload: Any,
1500
+ ) -> dict[str, str]:
1501
+ import base64
1502
+ import hashlib
1503
+ import hmac
1504
+
1505
+ ts = int(time.time())
1506
+ serialized = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
1507
+ msg = f"{ts}{method.upper()}{path}{serialized}"
1508
+ secret_bytes = base64.urlsafe_b64decode(api_secret)
1509
+ sig = hmac.new(secret_bytes, msg.encode("utf-8"), hashlib.sha256).digest()
1510
+ sign_b64 = base64.urlsafe_b64encode(sig).decode("utf-8")
1511
+ return {
1512
+ "POLY_ADDRESS": signer_addr,
1513
+ "POLY_SIGNATURE": sign_b64,
1514
+ "POLY_TIMESTAMP": str(ts),
1515
+ "POLY_API_KEY": api_key,
1516
+ "POLY_PASSPHRASE": api_passphrase,
1517
+ "Content-Type": "application/json",
1518
+ }
1519
+
1520
+ async def _submit_forward_requests(self, requests_payload: Sequence[Mapping[str, Any]]) -> Any:
1521
+ cfg = self._forward_orders_cfg
1522
+ if not cfg:
1523
+ raise RuntimeError("forward is not mounted")
1524
+ session: aiohttp.ClientSession = getattr(self.client, "_session", None)
1525
+ if session is None:
1526
+ raise RuntimeError("pybotters client session missing")
1527
+
1528
+ if cfg["mode"] == "rest":
1529
+ endpoint = cfg["url"]
1530
+ async with session.post(endpoint, json={"requests": list(requests_payload)}) as resp:
1531
+ if resp.status >= 400:
1532
+ text = await resp.text()
1533
+ raise RuntimeError(f"Forward batch failed: {resp.status} {text}")
1534
+ try:
1535
+ return await resp.json()
1536
+ except Exception:
1537
+ return await resp.text()
1538
+
1539
+ await self._ensure_forward_ws_connected(cfg)
1540
+ state = self._forward_ws
1541
+ ws: aiohttp.ClientWebSocketResponse | None = state.get("ws") if state else None
1542
+ if ws is None or ws.closed:
1543
+ raise RuntimeError("forward ws not connected")
1544
+
1545
+ req_id = f"hq-{int(time.time() * 1000)}"
1546
+ msg_type = "batch" if cfg.get("batch_mode") == "batch" else "batch-step"
1547
+ payload: dict[str, Any] = {
1548
+ "type": msg_type,
1549
+ "id": req_id,
1550
+ "requests": list(requests_payload),
1551
+ }
1552
+ if msg_type == "batch":
1553
+ payload["concurrency"] = int(cfg.get("concurrency", 5))
1554
+ await ws.send_json(payload)
1555
+ return {
1556
+ "mode": "ws",
1557
+ "queued": True,
1558
+ "request_id": req_id,
1559
+ "batch_mode": msg_type,
1560
+ "count": len(requests_payload),
1561
+ }
1562
+
1358
1563
  async def post_orders(
1359
1564
  self,
1360
1565
  orders: Iterable[tuple[Mapping[str, Any], str]],
@@ -1444,6 +1649,41 @@ class Polymarket:
1444
1649
  }
1445
1650
  )
1446
1651
 
1652
+ if self._forward_orders_cfg:
1653
+ session: aiohttp.ClientSession = getattr(self.client, "_session", None)
1654
+ if session is None:
1655
+ raise RuntimeError("pybotters client session missing")
1656
+ creds = getattr(session, "_polymarket_api_creds", None)
1657
+ if not creds:
1658
+ raise RuntimeError("Polymarket API creds missing; call create_or_derive_api_creds")
1659
+ api_key = creds.get("api_key")
1660
+ api_secret = creds.get("api_secret")
1661
+ api_passphrase = creds.get("api_passphrase")
1662
+ if not (api_key and api_secret and api_passphrase):
1663
+ raise RuntimeError("Polymarket API creds incomplete")
1664
+
1665
+ target_url = f"{self.rest_api.rstrip('/')}/order"
1666
+ requests_payload: list[dict[str, Any]] = []
1667
+ for payload in result_body:
1668
+ headers = self._build_l2_signed_headers(
1669
+ signer_addr=signer_addr,
1670
+ api_key=api_key,
1671
+ api_secret=api_secret,
1672
+ api_passphrase=api_passphrase,
1673
+ method="POST",
1674
+ path="/order",
1675
+ payload=payload,
1676
+ )
1677
+ requests_payload.append(
1678
+ {
1679
+ "method": "POST",
1680
+ "url": target_url,
1681
+ "headers": headers,
1682
+ "body": payload,
1683
+ }
1684
+ )
1685
+ return await self._submit_forward_requests(requests_payload)
1686
+
1447
1687
  return await self._signed_request_via_session("POST", "/orders", result_body)
1448
1688
 
1449
1689
  async def cancel(self, order_id: str) -> Any:
@@ -1452,11 +1692,48 @@ class Polymarket:
1452
1692
  """
1453
1693
  return await self._signed_request_via_session("DELETE", "/order", {"orderID": order_id})
1454
1694
 
1455
- async def cancel_orders(self, order_ids: Sequence[str]) -> Any:
1695
+ async def cancel_orders(
1696
+ self,
1697
+ order_ids: Sequence[str],
1698
+ ) -> Any:
1456
1699
  """
1457
1700
  {'not_canceled': {}, 'canceled': ['0xb3507e9fda9541c3e038afcb4f24b96efcfa667d46cf5e9e52c41620711818df', '0x4b9c3ee4dee8653f15f716653e8ac83f0a086a38597e6ec4b72be2389c79b8b4']}
1458
1701
  """
1459
- return await self._signed_request_via_session("DELETE", "/orders", list(order_ids))
1702
+ payload = list(order_ids)
1703
+ if self._forward_orders_cfg:
1704
+ session: aiohttp.ClientSession = getattr(self.client, "_session", None)
1705
+ if session is None:
1706
+ raise RuntimeError("pybotters client session missing")
1707
+ creds = getattr(session, "_polymarket_api_creds", None)
1708
+ if not creds:
1709
+ raise RuntimeError("Polymarket API creds missing; call create_or_derive_api_creds")
1710
+ api_key = creds.get("api_key")
1711
+ api_secret = creds.get("api_secret")
1712
+ api_passphrase = creds.get("api_passphrase")
1713
+ if not (api_key and api_secret and api_passphrase):
1714
+ raise RuntimeError("Polymarket API creds incomplete")
1715
+
1716
+ _, _, signer_addr = self._get_signing_context()
1717
+ headers = self._build_l2_signed_headers(
1718
+ signer_addr=signer_addr,
1719
+ api_key=api_key,
1720
+ api_secret=api_secret,
1721
+ api_passphrase=api_passphrase,
1722
+ method="DELETE",
1723
+ path="/orders",
1724
+ payload=payload,
1725
+ )
1726
+ requests_payload = [
1727
+ {
1728
+ "method": "DELETE",
1729
+ "url": f"{self.rest_api.rstrip('/')}/orders",
1730
+ "headers": headers,
1731
+ "body": payload,
1732
+ }
1733
+ ]
1734
+ return await self._submit_forward_requests(requests_payload)
1735
+
1736
+ return await self._signed_request_via_session("DELETE", "/orders", payload)
1460
1737
 
1461
1738
  async def cancel_all(self) -> Any:
1462
1739
  """
@@ -1846,14 +2123,22 @@ class Polymarket:
1846
2123
  if dry_run:
1847
2124
  results.append({**item, "tx": None, "dry_run": True})
1848
2125
  continue
1849
- tx_hash = await asyncio.to_thread(
1850
- self._claim_via_safe_sync,
1851
- candidates,
1852
- item["condition_id"],
1853
- item["index_sets"],
1854
- gas,
1855
- verbose,
1856
- )
2126
+ try:
2127
+ tx_hash = await asyncio.to_thread(
2128
+ self._claim_via_safe_sync,
2129
+ candidates,
2130
+ item["condition_id"],
2131
+ item["index_sets"],
2132
+ gas,
2133
+ verbose,
2134
+ )
2135
+ except Exception as exc:
2136
+ raise RuntimeError(
2137
+ "claim_positions failed for "
2138
+ f"condition_id={item.get('condition_id')} "
2139
+ f"index_sets={item.get('index_sets')} "
2140
+ f"size={item.get('size')}: {exc}"
2141
+ ) from exc
1857
2142
  result_row = {**item, "tx": tx_hash, "dry_run": False}
1858
2143
  if include_receipt:
1859
2144
  try:
@@ -1976,22 +2261,7 @@ class Polymarket:
1976
2261
  last_error: Exception | None = None
1977
2262
  w3 = None
1978
2263
  rpc_used = None
1979
- for url in candidates:
1980
- try:
1981
- w3 = Web3(Web3.HTTPProvider(url, request_kwargs={"timeout": 30}))
1982
- with suppress(Exception):
1983
- from web3.middleware import geth_poa_middleware, ExtraDataToPOAMiddleware
1984
- try:
1985
- w3.middleware_onion.inject(geth_poa_middleware, layer=0)
1986
- except Exception:
1987
- w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
1988
- rpc_used = url
1989
- break
1990
- except Exception as exc:
1991
- last_error = exc
1992
- continue
1993
- if w3 is None:
1994
- raise RuntimeError(f"All RPC endpoints failed: {candidates}") from last_error
2264
+ safe_nonce = None
1995
2265
 
1996
2266
  pk_env = os.getenv("PK")
1997
2267
  if pk_env:
@@ -2004,23 +2274,11 @@ class Polymarket:
2004
2274
  raise RuntimeError("Safe/proxy wallet address未知, 请在 apis['polymarket'][2] 或构造函数 funder 设置")
2005
2275
 
2006
2276
  ctf_addr = self._contracts(self.chain_id, False)["conditional_tokens"]
2007
- ctf = w3.eth.contract(address=w3.to_checksum_address(ctf_addr), abi=CONDITIONAL_TOKENS_ABI)
2008
- safe = w3.eth.contract(address=w3.to_checksum_address(safe_addr), abi=SAFE_ABI)
2009
2277
 
2010
2278
  cond_bytes = bytes.fromhex(condition_id.replace("0x", ""))
2011
2279
  if len(cond_bytes) != 32:
2012
2280
  raise ValueError("condition_id must be 32-byte hex string")
2013
2281
 
2014
- redeem_calldata = ctf.encode_abi(
2015
- "redeemPositions",
2016
- args=[
2017
- w3.to_checksum_address(USDC_CONTRACT),
2018
- bytes(32),
2019
- cond_bytes,
2020
- index_sets,
2021
- ],
2022
- )
2023
-
2024
2282
  safe_tx_gas = 0
2025
2283
  base_gas = 0
2026
2284
  gas_price = 0
@@ -2029,13 +2287,55 @@ class Polymarket:
2029
2287
  value = 0
2030
2288
  operation = 0 # CALL
2031
2289
 
2032
- try:
2033
- safe_nonce = safe.functions.nonce().call()
2034
- except Web3RPCError as exc:
2035
- # 速率限制等错误直接抛出,便于调用层切换 RPC
2036
- raise RuntimeError(f"获取 Safe nonce 失败 (rpc={rpc_used}): {exc}") from exc
2037
- except Exception as exc:
2038
- raise RuntimeError("无法获取 Safe nonce, 请确认 funder 地址为有效 Safe") from exc
2290
+ ctf = None
2291
+ safe = None
2292
+ redeem_calldata = None
2293
+ for url in candidates:
2294
+ try:
2295
+ w3_candidate = Web3(Web3.HTTPProvider(url, request_kwargs={"timeout": 30}))
2296
+ with suppress(Exception):
2297
+ from web3.middleware import geth_poa_middleware, ExtraDataToPOAMiddleware
2298
+ try:
2299
+ w3_candidate.middleware_onion.inject(geth_poa_middleware, layer=0)
2300
+ except Exception:
2301
+ w3_candidate.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
2302
+
2303
+ ctf_candidate = w3_candidate.eth.contract(
2304
+ address=w3_candidate.to_checksum_address(ctf_addr),
2305
+ abi=CONDITIONAL_TOKENS_ABI,
2306
+ )
2307
+ safe_candidate = w3_candidate.eth.contract(
2308
+ address=w3_candidate.to_checksum_address(safe_addr),
2309
+ abi=SAFE_ABI,
2310
+ )
2311
+ redeem_calldata_candidate = ctf_candidate.encode_abi(
2312
+ "redeemPositions",
2313
+ args=[
2314
+ w3_candidate.to_checksum_address(USDC_CONTRACT),
2315
+ bytes(32),
2316
+ cond_bytes,
2317
+ index_sets,
2318
+ ],
2319
+ )
2320
+ nonce_candidate = safe_candidate.functions.nonce().call()
2321
+
2322
+ w3 = w3_candidate
2323
+ rpc_used = url
2324
+ ctf = ctf_candidate
2325
+ safe = safe_candidate
2326
+ redeem_calldata = redeem_calldata_candidate
2327
+ safe_nonce = nonce_candidate
2328
+ break
2329
+ except Exception as exc:
2330
+ last_error = exc
2331
+ if verbose:
2332
+ print(f"[claim] rpc nonce probe failed: {url} -> {exc}")
2333
+ continue
2334
+
2335
+ if w3 is None or safe is None or ctf is None or redeem_calldata is None or safe_nonce is None:
2336
+ raise RuntimeError(
2337
+ f"无法获取 Safe nonce (tried rpc={candidates}); last_error={last_error}"
2338
+ ) from last_error
2039
2339
 
2040
2340
  try:
2041
2341
  safe_tx_hash = safe.functions.getTransactionHash(
@@ -2065,43 +2365,71 @@ class Polymarket:
2065
2365
  try:
2066
2366
  acct = _A.from_key(private_key)
2067
2367
  sender = acct.address
2068
- # 使用 pending 避免重复使用已在 mempool 的 nonce 触发 replacement underpriced
2069
- nonce = w3.eth.get_transaction_count(sender)
2070
- gas_price_chain = w3.eth.gas_price
2071
2368
  except Exception as exc:
2072
- raise RuntimeError("获取 sender nonce/gas_price 失败") from exc
2073
-
2074
- tx = safe.functions.execTransaction(
2075
- w3.to_checksum_address(ctf_addr),
2076
- value,
2077
- redeem_calldata,
2078
- operation,
2079
- safe_tx_gas,
2080
- base_gas,
2081
- gas_price,
2082
- w3.to_checksum_address(gas_token),
2083
- w3.to_checksum_address(refund_receiver),
2084
- sig_bytes,
2085
- ).build_transaction(
2086
- {
2087
- "from": sender,
2088
- "nonce": nonce,
2089
- "gas": gas,
2090
- "gasPrice": gas_price_chain,
2091
- }
2092
- )
2369
+ raise RuntimeError("获取 sender 地址失败") from exc
2093
2370
 
2094
- signed_tx = w3.eth.account.sign_transaction(tx, private_key)
2095
2371
  send_errors: list[str] = []
2096
2372
  for attempt in range(3):
2097
2373
  try:
2374
+ nonce = w3.eth.get_transaction_count(sender, "pending")
2375
+ gas_price_chain = int(w3.eth.gas_price)
2376
+ if attempt > 0:
2377
+ # retry 时小幅提价,减少卡在同价位的概率
2378
+ gas_price_chain = int(gas_price_chain * (1.0 + 0.10 * attempt))
2379
+
2380
+ exec_tx = safe.functions.execTransaction(
2381
+ w3.to_checksum_address(ctf_addr),
2382
+ value,
2383
+ redeem_calldata,
2384
+ operation,
2385
+ safe_tx_gas,
2386
+ base_gas,
2387
+ gas_price,
2388
+ w3.to_checksum_address(gas_token),
2389
+ w3.to_checksum_address(refund_receiver),
2390
+ sig_bytes,
2391
+ )
2392
+ try:
2393
+ simulated = exec_tx.call({"from": sender})
2394
+ except Exception as sim_exc:
2395
+ simulated = None
2396
+ if verbose:
2397
+ print(f"[claim] preflight call error (continue): {sim_exc}")
2398
+ if simulated is False:
2399
+ err = (
2400
+ f"Safe preflight failed (execTransaction call returned false): "
2401
+ f"safe_nonce={safe_nonce} sender_nonce={nonce}"
2402
+ )
2403
+ send_errors.append(err)
2404
+ if verbose:
2405
+ print(f"Safe redeem attempt {attempt+1} failed: {err}")
2406
+ break
2407
+
2408
+ tx = exec_tx.build_transaction(
2409
+ {
2410
+ "from": sender,
2411
+ "nonce": nonce,
2412
+ "gas": gas,
2413
+ "gasPrice": gas_price_chain,
2414
+ }
2415
+ )
2416
+
2417
+ signed_tx = w3.eth.account.sign_transaction(tx, private_key)
2098
2418
  raw_tx = getattr(signed_tx, "rawTransaction", None) or getattr(signed_tx, "raw_transaction", None)
2099
2419
  if raw_tx is None: # pragma: no cover
2100
2420
  raise AttributeError("Signed transaction missing rawTransaction/raw_transaction")
2101
2421
  tx_hash = w3.eth.send_raw_transaction(raw_tx)
2102
2422
  receipt = w3.eth.wait_for_transaction_receipt(tx_hash, poll_latency=2, timeout=180)
2103
2423
  if receipt.get("status") != 1:
2104
- raise RuntimeError(f"Safe redeemPositions failed status!=1: {tx_hash.hex()}")
2424
+ err = (
2425
+ f"Safe redeemPositions reverted on-chain: tx={tx_hash.hex()} "
2426
+ f"safe_nonce={safe_nonce} sender_nonce={nonce}"
2427
+ )
2428
+ send_errors.append(err)
2429
+ if verbose:
2430
+ print(f"Safe redeem attempt {attempt+1} failed: {err}")
2431
+ # 已经上链并回执失败,继续重试通常只会制造 nonce 噪音
2432
+ break
2105
2433
  if verbose:
2106
2434
  print(
2107
2435
  {
@@ -694,7 +694,7 @@ wheels = [
694
694
 
695
695
  [[package]]
696
696
  name = "hyperquant"
697
- version = "1.52"
697
+ version = "1.54"
698
698
  source = { editable = "." }
699
699
  dependencies = [
700
700
  { name = "aiohttp" },
File without changes
File without changes
File without changes