hyperquant 1.42__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.42 → hyperquant-1.43}/PKG-INFO +1 -1
- {hyperquant-1.42 → hyperquant-1.43}/pyproject.toml +1 -1
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/polymarket.py +392 -6
- {hyperquant-1.42 → hyperquant-1.43}/uv.lock +1 -1
- {hyperquant-1.42 → hyperquant-1.43}/.gitignore +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/README.md +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/requirements-dev.lock +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/requirements.lock +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/__init__.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/auth.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/bitget.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/bitmart.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/coinw.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/deepcoin.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/edgex.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/hyperliquid.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/lbank.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/lib/hpstore.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/lib/hyper_types.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/lib/util.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/lighter.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/models/apexpro.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/models/bitget.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/models/bitmart.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/models/coinw.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/models/deepcoin.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/models/edgex.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/models/hyperliquid.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/models/lbank.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/models/lighter.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/models/ourbit.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/models/polymarket.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/ourbit.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/broker/ws.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/core.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/datavison/_util.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/datavison/binance.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/datavison/coinglass.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/datavison/okx.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/db.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/draw.py +0 -0
- {hyperquant-1.42 → hyperquant-1.43}/src/hyperquant/logkit.py +0 -0
- {hyperquant-1.42 → 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
|
|
@@ -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):
|
|
@@ -1246,6 +1321,112 @@ class Polymarket:
|
|
|
1246
1321
|
position = position / 1e6
|
|
1247
1322
|
return position
|
|
1248
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
|
+
|
|
1249
1430
|
async def get_usdc_web3(
|
|
1250
1431
|
self,
|
|
1251
1432
|
wallet: str = None,
|
|
@@ -1286,6 +1467,189 @@ class Polymarket:
|
|
|
1286
1467
|
# ------------------------------------------------------------------
|
|
1287
1468
|
# Internal utilities
|
|
1288
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
|
+
|
|
1289
1653
|
async def _fetch_event(self, slug: str) -> dict | None:
|
|
1290
1654
|
resp = await self.client.get(GAMMA_EVENTS_API, params={"slug": slug})
|
|
1291
1655
|
payload = await resp.json()
|
|
@@ -1570,11 +1934,33 @@ class Polymarket:
|
|
|
1570
1934
|
return [pk, "", (api_key, api_secret, passphrase, chain_id, wallet_address)]
|
|
1571
1935
|
|
|
1572
1936
|
@lru_cache(maxsize=8)
|
|
1573
|
-
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
|
+
"""
|
|
1574
1945
|
from web3 import Web3
|
|
1575
1946
|
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
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
|
|
File without changes
|