hyperquant 1.48__py3-none-any.whl

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.

Potentially problematic release.


This version of hyperquant might be problematic. Click here for more details.

Files changed (42) hide show
  1. hyperquant/__init__.py +8 -0
  2. hyperquant/broker/auth.py +972 -0
  3. hyperquant/broker/bitget.py +311 -0
  4. hyperquant/broker/bitmart.py +720 -0
  5. hyperquant/broker/coinw.py +487 -0
  6. hyperquant/broker/deepcoin.py +651 -0
  7. hyperquant/broker/edgex.py +500 -0
  8. hyperquant/broker/hyperliquid.py +570 -0
  9. hyperquant/broker/lbank.py +661 -0
  10. hyperquant/broker/lib/edgex_sign.py +455 -0
  11. hyperquant/broker/lib/hpstore.py +252 -0
  12. hyperquant/broker/lib/hyper_types.py +48 -0
  13. hyperquant/broker/lib/polymarket/ctfAbi.py +721 -0
  14. hyperquant/broker/lib/polymarket/safeAbi.py +1138 -0
  15. hyperquant/broker/lib/util.py +22 -0
  16. hyperquant/broker/lighter.py +679 -0
  17. hyperquant/broker/models/apexpro.py +150 -0
  18. hyperquant/broker/models/bitget.py +359 -0
  19. hyperquant/broker/models/bitmart.py +635 -0
  20. hyperquant/broker/models/coinw.py +724 -0
  21. hyperquant/broker/models/deepcoin.py +809 -0
  22. hyperquant/broker/models/edgex.py +1053 -0
  23. hyperquant/broker/models/hyperliquid.py +284 -0
  24. hyperquant/broker/models/lbank.py +557 -0
  25. hyperquant/broker/models/lighter.py +868 -0
  26. hyperquant/broker/models/ourbit.py +1155 -0
  27. hyperquant/broker/models/polymarket.py +1071 -0
  28. hyperquant/broker/ourbit.py +550 -0
  29. hyperquant/broker/polymarket.py +2399 -0
  30. hyperquant/broker/ws.py +132 -0
  31. hyperquant/core.py +513 -0
  32. hyperquant/datavison/_util.py +18 -0
  33. hyperquant/datavison/binance.py +111 -0
  34. hyperquant/datavison/coinglass.py +237 -0
  35. hyperquant/datavison/okx.py +177 -0
  36. hyperquant/db.py +191 -0
  37. hyperquant/draw.py +1200 -0
  38. hyperquant/logkit.py +205 -0
  39. hyperquant/notikit.py +124 -0
  40. hyperquant-1.48.dist-info/METADATA +32 -0
  41. hyperquant-1.48.dist-info/RECORD +42 -0
  42. hyperquant-1.48.dist-info/WHEEL +4 -0
@@ -0,0 +1,972 @@
1
+ import base64
2
+ import hmac
3
+ import urllib.parse
4
+ import time
5
+ import hashlib
6
+ from functools import lru_cache
7
+ from typing import Any
8
+ from aiohttp import ClientWebSocketResponse, FormData, JsonPayload
9
+ from multidict import CIMultiDict
10
+ from yarl import URL
11
+ import pybotters
12
+ import json as pyjson
13
+ from urllib.parse import urlencode
14
+ from datetime import datetime, timezone
15
+ from eth_account import Account
16
+ from eth_account.messages import encode_typed_data
17
+ from eth_utils import keccak, to_checksum_address
18
+ import secrets
19
+ from random import random
20
+
21
+
22
+ POLY_ADDRESS = "POLY_ADDRESS"
23
+ POLY_SIGNATURE = "POLY_SIGNATURE"
24
+ POLY_TIMESTAMP = "POLY_TIMESTAMP"
25
+ POLY_NONCE = "POLY_NONCE"
26
+ POLY_API_KEY = "POLY_API_KEY"
27
+ POLY_PASSPHRASE = "POLY_PASSPHRASE"
28
+ CLOB_AUTH_DOMAIN = {"name": "ClobAuthDomain", "version": "1"}
29
+ CLOB_AUTH_MESSAGE = "This message attests that I control the given wallet"
30
+
31
+ _PM_DOMAIN_TYPEHASH = keccak(b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
32
+ _PM_ORDER_TYPEHASH = keccak(
33
+ b"Order(uint256 salt,address maker,address signer,address taker,uint256 tokenId,uint256 makerAmount,uint256 takerAmount,uint256 expiration,uint256 nonce,uint256 feeRateBps,uint8 side,uint8 signatureType)"
34
+ )
35
+ _PM_DOMAIN_NAME_HASH = keccak(text="Polymarket CTF Exchange")
36
+ _PM_DOMAIN_VERSION_HASH = keccak(text="1")
37
+
38
+
39
+ def _int_to_32(val: int) -> bytes:
40
+ return int(val).to_bytes(32, "big", signed=False)
41
+
42
+
43
+ def _addr_to_32(addr: str) -> bytes:
44
+ # Normalize to checksum to match eth signing behavior
45
+ normalized = to_checksum_address(addr)
46
+ return int(normalized, 16).to_bytes(32, "big", signed=False)
47
+
48
+
49
+ @lru_cache(maxsize=32)
50
+ def _pm_domain_separator(chain_id: int, verifying_contract: str) -> bytes:
51
+ return keccak(
52
+ _PM_DOMAIN_TYPEHASH
53
+ + _PM_DOMAIN_NAME_HASH
54
+ + _PM_DOMAIN_VERSION_HASH
55
+ + _int_to_32(chain_id)
56
+ + _addr_to_32(verifying_contract),
57
+ )
58
+
59
+
60
+ def _pm_order_hash(message: dict[str, Any]) -> bytes:
61
+ """Manually hash the Polymarket Order struct for EIP-712."""
62
+ return keccak(
63
+ _PM_ORDER_TYPEHASH
64
+ + _int_to_32(message["salt"])
65
+ + _addr_to_32(message["maker"])
66
+ + _addr_to_32(message["signer"])
67
+ + _addr_to_32(message["taker"])
68
+ + _int_to_32(message["tokenId"])
69
+ + _int_to_32(message["makerAmount"])
70
+ + _int_to_32(message["takerAmount"])
71
+ + _int_to_32(message["expiration"])
72
+ + _int_to_32(message["nonce"])
73
+ + _int_to_32(message["feeRateBps"])
74
+ + _int_to_32(message["side"])
75
+ + _int_to_32(message["signatureType"]),
76
+ )
77
+
78
+
79
+ def md5_hex(s: str) -> str:
80
+ return hashlib.md5(s.encode("utf-8")).hexdigest()
81
+
82
+
83
+
84
+ def serialize(obj, prefix=''):
85
+ """
86
+ Python 版 UK/v:递归排序 + urlencode + 展平
87
+ """
88
+ def _serialize(obj, prefix=''):
89
+ if obj is None:
90
+ return []
91
+ if isinstance(obj, dict):
92
+ items = []
93
+ for k in sorted(obj.keys()):
94
+ v = obj[k]
95
+ n = f"{prefix}[{k}]" if prefix else k
96
+ items.extend(_serialize(v, n))
97
+ return items
98
+ elif isinstance(obj, list):
99
+ # JS style: output key once, then join values by &
100
+ values = []
101
+ for v in obj:
102
+ if isinstance(v, dict):
103
+ # Recursively serialize dict, but drop key= part (just use value part)
104
+ sub = _serialize(v, prefix)
105
+ # sub is a list of key=value, but we want only value part
106
+ for s in sub:
107
+ # s is like 'key=value', need only value
108
+ parts = s.split('=', 1)
109
+ if len(parts) == 2:
110
+ values.append(parts[1])
111
+ else:
112
+ values.append(parts[0])
113
+ else:
114
+ # Handle booleans and empty strings
115
+ if isinstance(v, bool):
116
+ val = "true" if v else "false"
117
+ elif v == "":
118
+ val = ""
119
+ else:
120
+ val = str(v)
121
+ values.append(val)
122
+ return [f"{urllib.parse.quote(str(prefix))}={'&'.join(values)}"]
123
+ else:
124
+ # Handle booleans and empty strings
125
+ if isinstance(obj, bool):
126
+ val = "true" if obj else "false"
127
+ elif obj == "":
128
+ val = ""
129
+ else:
130
+ val = str(obj)
131
+ return [f"{urllib.parse.quote(str(prefix))}={val}"]
132
+ return "&".join(_serialize(obj, prefix))
133
+
134
+ # 🔑 Ourbit 的鉴权函数
135
+ class Auth:
136
+ @staticmethod
137
+ def edgex(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
138
+ method: str = args[0]
139
+ url: URL = args[1]
140
+ data = kwargs.get("data") or {}
141
+ headers: CIMultiDict = kwargs["headers"]
142
+
143
+ session = kwargs["session"]
144
+ api_key:str = session.__dict__["_apis"][pybotters.auth.Hosts.items[url.host].name][0]
145
+ secret = session.__dict__["_apis"][pybotters.auth.Hosts.items[url.host].name][1]
146
+ passphrase:str = session.__dict__["_apis"][pybotters.auth.Hosts.items[url.host].name][2]
147
+ passphrase = passphrase.split("-")[0]
148
+ timestamp = str(int(time.time() * 1000))
149
+ # timestamp = "1758535055061"
150
+
151
+ raw_body = ""
152
+ if data and method.upper() in ["POST", "PUT", "PATCH"] and data:
153
+ raw_body = serialize(data)
154
+ else:
155
+ raw_body = serialize(dict(url.query.items()))
156
+
157
+
158
+ secret_quoted = urllib.parse.quote(secret, safe="")
159
+ b64_secret = base64.b64encode(secret_quoted.encode("utf-8")).decode()
160
+ message = f"{timestamp}{method.upper()}{url.raw_path}{raw_body}"
161
+ sign = hmac.new(b64_secret.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).hexdigest()
162
+
163
+ sigh_header = {
164
+ "X-edgeX-Api-Key": api_key,
165
+ "X-edgeX-Passphrase": passphrase,
166
+ "X-edgeX-Signature": sign,
167
+ "X-edgeX-Timestamp": timestamp,
168
+ }
169
+ # ws单独进行签名
170
+ if headers.get("Upgrade") == "websocket":
171
+ json_str = pyjson.dumps(sigh_header, separators=(",", ":"))
172
+ b64_str = base64.b64encode(json_str.encode("utf-8")).decode("utf-8")
173
+ b64_str.replace("=", "")
174
+ headers.update({"Sec-WebSocket-Protocol": b64_str})
175
+ else:
176
+ headers.update(sigh_header)
177
+
178
+ if data:
179
+ kwargs.update({"data": JsonPayload(data)})
180
+
181
+ return args
182
+
183
+ @staticmethod
184
+ def ourbit(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
185
+ method: str = args[0]
186
+ url: URL = args[1]
187
+ data = kwargs.get("data") or {}
188
+ headers: CIMultiDict = kwargs["headers"]
189
+
190
+ # 从 session 里取 token
191
+ session = kwargs["session"]
192
+ token = session.__dict__["_apis"][pybotters.auth.Hosts.items[url.host].name][0]
193
+
194
+ # 时间戳 & body
195
+ now_ms = int(time.time() * 1000)
196
+ raw_body_for_sign = (
197
+ data
198
+ if isinstance(data, str)
199
+ else pyjson.dumps(data, separators=(",", ":"), ensure_ascii=False)
200
+ )
201
+
202
+ # 签名
203
+ mid_hash = md5_hex(f"{token}{now_ms}")[7:]
204
+ final_hash = md5_hex(f"{now_ms}{raw_body_for_sign}{mid_hash}")
205
+
206
+ # 设置 headers
207
+ headers.update(
208
+ {
209
+ "Authorization": token,
210
+ "Language": "Chinese",
211
+ "language": "Chinese",
212
+ "Content-Type": "application/json",
213
+ "x-ourbit-sign": final_hash,
214
+ "x-ourbit-nonce": str(now_ms),
215
+ }
216
+ )
217
+
218
+ # 更新 kwargs.body,保证发出去的与签名一致
219
+ kwargs.update({"data": raw_body_for_sign})
220
+
221
+ return args
222
+
223
+ @staticmethod
224
+ def ourbit_spot(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
225
+ method: str = args[0]
226
+ url: URL = args[1]
227
+ data = kwargs.get("data") or {}
228
+ headers: CIMultiDict = kwargs["headers"]
229
+
230
+ # 从 session 里取 token
231
+ session = kwargs["session"]
232
+ token = session.__dict__["_apis"][pybotters.auth.Hosts.items[url.host].name][0]
233
+ cookie = f"uc_token={token}; u_id={token}; "
234
+ headers.update({"cookie": cookie})
235
+ return args
236
+
237
+ @staticmethod
238
+ def lbank(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
239
+ method: str = args[0]
240
+ url: URL = args[1]
241
+ data = kwargs.get("data") or {}
242
+ headers: CIMultiDict = kwargs["headers"]
243
+
244
+ # 从 session 里取 api_key & secret
245
+ session = kwargs["session"]
246
+ token = session.__dict__["_apis"][pybotters.auth.Hosts.items[url.host].name][0]
247
+
248
+
249
+ # 设置 headers
250
+ headers.update(
251
+ {
252
+ "ex-language": 'zh-TW',
253
+ "ex-token": token,
254
+ "source": "4",
255
+ "versionflage": "true",
256
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0"
257
+ }
258
+ )
259
+
260
+ # 更新 kwargs.body,保证发出去的与签名一致
261
+ # kwargs.update({"data": raw_body_for_sign})
262
+
263
+ return args
264
+
265
+ @staticmethod
266
+ def deepcoin(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
267
+ method: str = args[0].upper()
268
+ url: URL = args[1]
269
+ headers: CIMultiDict = kwargs["headers"]
270
+
271
+ session = kwargs["session"]
272
+ creds = session.__dict__["_apis"][pybotters.auth.Hosts.items[url.host].name]
273
+ if len(creds) < 3:
274
+ raise RuntimeError("DeepCoin credentials must include api_key, secret, passphrase")
275
+ api_key, secret, passphrase = creds[0], creds[1], creds[2]
276
+
277
+ timestamp = datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
278
+
279
+ body_str = ""
280
+ json_body = kwargs.pop("json", None)
281
+ data_body = kwargs.get("data")
282
+ if method in {"POST", "PUT", "PATCH"}:
283
+ payload = json_body if json_body is not None else data_body
284
+ if payload is not None:
285
+ if isinstance(payload, (dict, list)):
286
+ body_str = pyjson.dumps(payload, separators=(",", ":"), ensure_ascii=False)
287
+ kwargs["data"] = body_str
288
+ else:
289
+ body_str = str(payload)
290
+ kwargs["data"] = body_str
291
+ else:
292
+ kwargs["data"] = body_str
293
+ else:
294
+ if json_body is not None:
295
+ # GET requests should not carry JSON bodies
296
+ kwargs.pop("json", None)
297
+ if data_body is not None:
298
+ kwargs["data"] = data_body
299
+
300
+ request_path = url.raw_path_qs or url.raw_path
301
+ message = f"{timestamp}{method}{request_path}{body_str}"
302
+ secret_bytes = secret.encode("utf-8") if isinstance(secret, str) else secret
303
+ signature = hmac.new(secret_bytes, message.encode("utf-8"), hashlib.sha256).digest()
304
+ sign_b64 = base64.b64encode(signature).decode()
305
+
306
+ headers.update(
307
+ {
308
+ "DC-ACCESS-KEY": api_key,
309
+ "DC-ACCESS-PASSPHRASE": passphrase,
310
+ "DC-ACCESS-TIMESTAMP": timestamp,
311
+ "DC-ACCESS-SIGN": sign_b64,
312
+ "Content-Type": headers.get("Content-Type", "application/json"),
313
+ }
314
+ )
315
+
316
+ return args
317
+
318
+ @staticmethod
319
+ def bitmart(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
320
+ method: str = args[0]
321
+ url: URL = args[1]
322
+ headers: CIMultiDict = kwargs["headers"]
323
+
324
+ session = kwargs["session"]
325
+ host_key = url.host
326
+ try:
327
+ api_name = pybotters.auth.Hosts.items[host_key].name
328
+ except (KeyError, AttributeError):
329
+ api_name = host_key
330
+
331
+ creds = session.__dict__.get("_apis", {}).get(api_name)
332
+ if not creds or len(creds) < 3:
333
+ raise RuntimeError("Bitmart credentials (accessKey, accessSalt, device) are required")
334
+
335
+ access_key = creds[0]
336
+ access_salt = creds[1]
337
+ access_salt = access_salt.decode("utf-8")
338
+ device = creds[2]
339
+ extra_cookie = creds[3] if len(creds) > 3 else None
340
+
341
+ cookie_parts = [
342
+ f"accessKey={access_key}",
343
+ f"accessSalt={access_salt}",
344
+ "hasDelegation=false",
345
+ "delegationType=0",
346
+ "delegationTypeList=[]",
347
+ ]
348
+ if extra_cookie:
349
+ if isinstance(extra_cookie, str) and extra_cookie:
350
+ cookie_parts.append(extra_cookie.strip(";"))
351
+
352
+ headers["cookie"] = "; ".join(cookie_parts)
353
+
354
+ headers.setdefault("x-bm-client", "WEB")
355
+ headers.setdefault("x-bm-contract", "2")
356
+ headers.setdefault("x-bm-device", device)
357
+ headers.setdefault("x-bm-timezone", "Asia/Shanghai")
358
+ headers.setdefault("x-bm-timezone-offset", "-480")
359
+ headers.setdefault("x-bm-tag", "")
360
+ headers.setdefault("x-bm-version", "5e13905")
361
+ headers.setdefault('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0')
362
+
363
+ ua = headers.get("User-Agent") or headers.get("user-agent")
364
+ if ua:
365
+ headers.setdefault("x-bm-ua", ua)
366
+
367
+ return args
368
+
369
+ @staticmethod
370
+ def bitmart_api(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
371
+ """Bitmart OpenAPI (futures v2) signing for api-cloud-v2.bitmart.com.
372
+
373
+ Spec per docs:
374
+ X-BM-SIGN = hex_lower(HMAC_SHA256(secret, timestamp + '#' + memo + '#' + payload_string))
375
+ - For POST: payload_string is the JSON body string
376
+ - For GET: payload_string is the query string (if any), otherwise empty
377
+ Headers required: X-BM-KEY, X-BM-TIMESTAMP, X-BM-SIGN
378
+ """
379
+ method: str = args[0]
380
+ url: URL = args[1]
381
+ headers: CIMultiDict = kwargs["headers"]
382
+
383
+ session = kwargs["session"]
384
+ try:
385
+ api_name = pybotters.auth.Hosts.items[url.host].name
386
+ except (KeyError, AttributeError):
387
+ api_name = url.host
388
+
389
+ creds = session.__dict__.get("_apis", {}).get(api_name)
390
+ if not creds or len(creds) < 3:
391
+ raise RuntimeError("Bitmart API credentials (access_key, secret, memo) are required")
392
+
393
+ access_key = creds[0]
394
+ secret = creds[1]
395
+ memo = creds[2]
396
+ if isinstance(secret, str):
397
+ secret_bytes = secret.encode("utf-8")
398
+ else:
399
+ secret_bytes = secret
400
+ if isinstance(memo, bytes):
401
+ memo = memo.decode("utf-8")
402
+
403
+ timestamp = str(int(time.time() * 1000))
404
+ method_upper = method.upper()
405
+
406
+ # Build query string
407
+ params = kwargs.get("params")
408
+ if isinstance(params, dict) and params:
409
+ query_items = [f"{k}={v}" for k, v in params.items() if v is not None]
410
+ query_string = "&".join(query_items)
411
+ else:
412
+ query_string = url.query_string
413
+
414
+ # Build body string for signing and ensure sending matches signature
415
+ body = None
416
+ body_str = ""
417
+ if method_upper == "GET":
418
+ body_str = query_string or ""
419
+ else:
420
+ # Prefer original JSON object if present for deterministic signing
421
+ if kwargs.get("json") is not None:
422
+ body = kwargs.get("json")
423
+ else:
424
+ body = kwargs.get("data")
425
+
426
+ # If upstream already turned JSON into JsonPayload, extract its value
427
+ if isinstance(body, JsonPayload):
428
+ body_value = getattr(body, "_value", None)
429
+ else:
430
+ body_value = body
431
+
432
+ if isinstance(body_value, (dict, list)):
433
+ # Compact JSON to avoid whitespace discrepancies and sign exact bytes we send
434
+ body_str = pyjson.dumps(body_value, separators=(",", ":"), ensure_ascii=False)
435
+ kwargs["data"] = body_str
436
+ kwargs.pop("json", None)
437
+ elif isinstance(body_value, (str, bytes)):
438
+ # Sign and send exactly this string/bytes
439
+ body_str = body_value.decode("utf-8") if isinstance(body_value, bytes) else body_value
440
+ kwargs["data"] = body_str
441
+ kwargs.pop("json", None)
442
+ elif body_value is None:
443
+ body_str = ""
444
+ else:
445
+ # Fallback: string conversion (should still be JSON-like)
446
+ body_str = str(body_value)
447
+ kwargs["data"] = body_str
448
+ kwargs.pop("json", None)
449
+
450
+ # Prehash format: timestamp#memo#payload
451
+ message = f"{timestamp}#{memo}#{body_str}"
452
+ signature_hex = hmac.new(secret_bytes, message.encode("utf-8"), hashlib.sha256).hexdigest()
453
+
454
+ headers.update(
455
+ {
456
+ "X-BM-KEY": access_key,
457
+ "X-BM-TIMESTAMP": timestamp,
458
+ "X-BM-SIGN": signature_hex,
459
+ "Content-Type": "application/json; charset=UTF-8",
460
+ }
461
+ )
462
+
463
+ return args
464
+
465
+ @staticmethod
466
+ def coinw(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
467
+ method: str = args[0]
468
+ url: URL = args[1]
469
+ headers: CIMultiDict = kwargs["headers"]
470
+
471
+ session = kwargs["session"]
472
+ try:
473
+ api_key, secret, _ = session.__dict__["_apis"][pybotters.auth.Hosts.items[url.host].name]
474
+ except (KeyError, ValueError):
475
+ raise RuntimeError("CoinW credentials (api_key, secret) are required")
476
+
477
+ timestamp = str(int(time.time() * 1000))
478
+ method_upper = method.upper()
479
+
480
+ params = kwargs.get("params")
481
+ query_string = ""
482
+ if isinstance(params, dict) and params:
483
+ query_items = [
484
+ f"{key}={value}"
485
+ for key, value in params.items()
486
+ if value is not None
487
+ ]
488
+ query_string = "&".join(query_items)
489
+ elif url.query_string:
490
+ query_string = url.query_string
491
+
492
+ body_str = ""
493
+
494
+ if method_upper == "GET":
495
+ body = None
496
+ data = None
497
+ else:
498
+ body = kwargs.get("json")
499
+ data = kwargs.get("data")
500
+ payload = body if body is not None else data
501
+ if isinstance(payload, (dict, list)):
502
+ body_str = pyjson.dumps(payload, separators=(",", ":"), ensure_ascii=False)
503
+ kwargs["data"] = body_str
504
+ kwargs.pop("json", None)
505
+ elif payload is not None:
506
+ body_str = str(payload)
507
+ kwargs["data"] = body_str
508
+ kwargs.pop("json", None)
509
+
510
+ if query_string:
511
+ path = f"{url.raw_path}?{query_string}"
512
+ else:
513
+ path = url.raw_path
514
+
515
+ message = f"{timestamp}{method_upper}{path}{body_str}"
516
+ signature = hmac.new(
517
+ secret, message.encode("utf-8"), hashlib.sha256
518
+ ).digest()
519
+ signature_b64 = base64.b64encode(signature).decode("ascii")
520
+
521
+ headers.update(
522
+ {
523
+ "sign": signature_b64,
524
+ "api_key": api_key,
525
+ "timestamp": timestamp,
526
+ }
527
+ )
528
+
529
+ if method_upper in {"POST", "PUT", "PATCH", "DELETE"} and "data" in kwargs:
530
+ headers.setdefault("Content-Type", "application/json")
531
+
532
+ return args
533
+
534
+ @staticmethod
535
+ def polymarket_back(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
536
+ method: str = args[0].upper()
537
+ url: URL = args[1]
538
+ headers: CIMultiDict = kwargs["headers"]
539
+
540
+ session = kwargs["session"]
541
+ try:
542
+ api_name = pybotters.auth.Hosts.items[url.host].name
543
+ creds = session.__dict__["_apis"].get(api_name)
544
+ except (KeyError, AttributeError):
545
+ return args
546
+
547
+ if not creds:
548
+ return args
549
+
550
+ if isinstance(creds, tuple):
551
+ creds = list(creds)
552
+ session.__dict__["_apis"][api_name] = creds
553
+
554
+ private_key = creds[0] if len(creds) > 0 and creds[0] else None
555
+ if private_key:
556
+ pk_str = str(private_key)
557
+ if not pk_str.startswith("0x"):
558
+ pk_str = f"0x{pk_str}"
559
+ try:
560
+ session.__dict__["_apis"][api_name][0] = pk_str
561
+ except Exception:
562
+ pass
563
+ private_key = pk_str
564
+
565
+ packed_extra = creds[2] if len(creds) > 2 else None
566
+ packed_api_key = packed_api_secret = packed_passphrase = None
567
+ packed_chain_id = packed_wallet = None
568
+ if isinstance(packed_extra, (list, tuple)):
569
+ def _packed_value(idx: int):
570
+ if idx >= len(packed_extra):
571
+ return None
572
+ value = packed_extra[idx]
573
+ if isinstance(value, str):
574
+ value = value.strip()
575
+ return value or None
576
+
577
+ packed_api_key = _packed_value(0)
578
+ packed_api_secret = _packed_value(1)
579
+ packed_passphrase = _packed_value(2)
580
+ packed_chain_id = _packed_value(3)
581
+ packed_wallet = _packed_value(4)
582
+ elif isinstance(packed_extra, str):
583
+ packed_wallet = packed_extra or None
584
+
585
+ existing_chain_id = session.__dict__.get("_polymarket_chain_id")
586
+ if existing_chain_id is None:
587
+ chain_id = 137
588
+ if packed_chain_id is not None:
589
+ try:
590
+ chain_id = int(packed_chain_id)
591
+ except (TypeError, ValueError):
592
+ chain_id = 137
593
+ session.__dict__["_polymarket_chain_id"] = chain_id
594
+ else:
595
+ chain_id = existing_chain_id
596
+
597
+ api_meta = session.__dict__.get("_polymarket_api_creds") or {}
598
+ if (not api_meta.get("api_key") or not api_meta.get("api_secret") or not api_meta.get("api_passphrase")) and (
599
+ packed_api_key and packed_api_secret and packed_passphrase
600
+ ):
601
+ api_meta = {
602
+ "api_key": packed_api_key,
603
+ "api_secret": packed_api_secret,
604
+ "api_passphrase": packed_passphrase,
605
+ }
606
+ session.__dict__["_polymarket_api_creds"] = api_meta
607
+
608
+ if packed_wallet and len(creds) > 2 and isinstance(creds[2], (list, tuple)):
609
+ creds[2] = packed_wallet
610
+
611
+ api_key = api_meta.get("api_key")
612
+ api_secret = api_meta.get("api_secret")
613
+ api_passphrase = api_meta.get("api_passphrase")
614
+
615
+ raw_path = url.raw_path
616
+ path_with_query = url.raw_path_qs or raw_path
617
+ level1_only_paths = {"/auth/api-key", "/auth/derive-api-key"}
618
+ requires_level1 = raw_path in level1_only_paths
619
+
620
+ if requires_level1:
621
+ if not private_key:
622
+ raise RuntimeError("Polymarket private key is required for auth endpoints")
623
+ params = kwargs.get("params")
624
+ nonce = 0
625
+ if isinstance(params, dict) and "nonce" in params:
626
+ try:
627
+ nonce = int(params.pop("nonce"))
628
+ except (TypeError, ValueError):
629
+ nonce = 0
630
+ if not params:
631
+ kwargs.pop("params", None)
632
+ timestamp = int(time.time())
633
+ typed_data = {
634
+ "types": {
635
+ "EIP712Domain": [
636
+ {"name": "name", "type": "string"},
637
+ {"name": "version", "type": "string"},
638
+ {"name": "chainId", "type": "uint256"},
639
+ ],
640
+ "ClobAuth": [
641
+ {"name": "address", "type": "address"},
642
+ {"name": "timestamp", "type": "string"},
643
+ {"name": "nonce", "type": "uint256"},
644
+ {"name": "message", "type": "string"},
645
+ ],
646
+ },
647
+ "domain": {**CLOB_AUTH_DOMAIN, "chainId": chain_id},
648
+ "primaryType": "ClobAuth",
649
+ "message": {
650
+ "address": Account.from_key(private_key).address,
651
+ "timestamp": str(timestamp),
652
+ "nonce": nonce,
653
+ "message": CLOB_AUTH_MESSAGE,
654
+ },
655
+ }
656
+ encoded = encode_typed_data(full_message=typed_data)
657
+ signature = Account.sign_message(encoded, private_key).signature.hex()
658
+ if not signature.startswith("0x"):
659
+ signature = f"0x{signature}"
660
+
661
+ headers.update(
662
+ {
663
+ POLY_ADDRESS: Account.from_key(private_key).address,
664
+ POLY_SIGNATURE: signature,
665
+ POLY_TIMESTAMP: str(timestamp),
666
+ POLY_NONCE: str(int(nonce)),
667
+ }
668
+ )
669
+ return args
670
+
671
+ if not (private_key and api_key and api_secret and api_passphrase):
672
+ return args
673
+
674
+ timestamp = int(time.time())
675
+ json_body = kwargs.pop("json", None)
676
+ data_body = kwargs.get("data")
677
+ body_for_sign = None
678
+ body_str = ""
679
+
680
+ if method in {"POST", "PUT", "PATCH", "DELETE"}:
681
+ # Preserve Python object for signature to mimic official client
682
+ if json_body is not None:
683
+ body_for_sign = json_body
684
+ body_str = pyjson.dumps(json_body, separators=(",", ":"), ensure_ascii=False)
685
+ kwargs["data"] = body_str
686
+ elif isinstance(data_body, (dict, list)):
687
+ body_for_sign = data_body
688
+ body_str = pyjson.dumps(data_body, separators=(",", ":"), ensure_ascii=False)
689
+ kwargs["data"] = body_str
690
+ elif data_body is not None:
691
+ body_for_sign = data_body
692
+ body_str = str(data_body)
693
+ kwargs["data"] = body_str
694
+ else:
695
+ kwargs["data"] = ""
696
+ else:
697
+ if data_body is not None and not isinstance(data_body, (FormData, JsonPayload)):
698
+ kwargs.pop("data", None)
699
+ body_str = ""
700
+ body_for_sign = None
701
+
702
+ try:
703
+ secret_bytes = base64.urlsafe_b64decode(api_secret)
704
+ except Exception:
705
+ secret_bytes = base64.urlsafe_b64decode(str(api_secret))
706
+
707
+ # Build message like py_clob_client: str(body).replace('\'', '"') when body present
708
+ if body_for_sign is not None:
709
+ serialized = str(body_for_sign).replace("'", '"')
710
+ else:
711
+ serialized = ""
712
+ message = f"{timestamp}{method}{raw_path}{serialized}"
713
+ signature = hmac.new(secret_bytes, message.encode("utf-8"), hashlib.sha256).digest()
714
+ sign_b64 = base64.urlsafe_b64encode(signature).decode("utf-8")
715
+
716
+ headers.update(
717
+ {
718
+ POLY_ADDRESS: Account.from_key(private_key).address,
719
+ POLY_SIGNATURE: sign_b64,
720
+ POLY_TIMESTAMP: str(timestamp),
721
+ POLY_API_KEY: api_key,
722
+ POLY_PASSPHRASE: api_passphrase,
723
+ }
724
+ )
725
+ if method in {"POST", "PUT", "PATCH", "DELETE"}:
726
+ headers.setdefault("Content-Type", "application/json")
727
+
728
+ return args
729
+
730
+ @staticmethod
731
+ def polymarket(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
732
+ """Alias kept for backward compatibility with pybotters host registration."""
733
+ return Auth.polymarket_back(args, kwargs)
734
+
735
+ # --------------------------
736
+ # Polymarket order signing
737
+ # --------------------------
738
+ @staticmethod
739
+ def sign_polymarket_order(
740
+ *,
741
+ private_key: str,
742
+ chain_id: int,
743
+ exchange_address: str,
744
+ order: dict[str, Any],
745
+ ) -> dict[str, Any]:
746
+ """
747
+ Sign a Polymarket CTF order via EIP-712 and return a submission-ready dict.
748
+
749
+ Expects 'order' to contain raw fields:
750
+ - maker, signer, taker (addresses)
751
+ - tokenId, makerAmount, takerAmount, expiration, nonce, feeRateBps (ints)
752
+ - side (0=BUY, 1=SELL), signatureType (int), salt (optional)
753
+ """
754
+ # Prepare message fields
755
+ # Salt generation mirrors py_order_utils (round(now * random()))
756
+ try:
757
+ now_ts = datetime.now().replace(tzinfo=timezone.utc).timestamp()
758
+ generated_salt = round(now_ts * random())
759
+ except Exception:
760
+ generated_salt = int(time.time())
761
+ side = int(order.get("side", 0))
762
+ signature_type = int(order.get("signatureType", 1))
763
+ # Normalize addresses to checksum
764
+ try:
765
+ from web3 import Web3
766
+ maker_addr = Web3.to_checksum_address(order["maker"]) if order.get("maker") else None
767
+ signer_addr = Web3.to_checksum_address(order.get("signer") or Account.from_key(private_key).address)
768
+ taker_addr = Web3.to_checksum_address(order.get("taker") or "0x0000000000000000000000000000000000000000")
769
+ exchange_addr = Web3.to_checksum_address(exchange_address)
770
+ except Exception:
771
+ maker_addr = order.get("maker")
772
+ signer_addr = order.get("signer") or Account.from_key(private_key).address
773
+ taker_addr = order.get("taker") or "0x0000000000000000000000000000000000000000"
774
+ exchange_addr = exchange_address
775
+
776
+ message = {
777
+ "salt": int(order.get("salt") or generated_salt),
778
+ "maker": maker_addr,
779
+ "signer": signer_addr,
780
+ "taker": taker_addr,
781
+ "tokenId": int(order["tokenId"]),
782
+ "makerAmount": int(order["makerAmount"]),
783
+ "takerAmount": int(order["takerAmount"]),
784
+ "expiration": int(order.get("expiration", 0)),
785
+ "nonce": int(order.get("nonce", 0)),
786
+ "feeRateBps": int(order.get("feeRateBps", 0)),
787
+ "side": side,
788
+ "signatureType": signature_type,
789
+ }
790
+
791
+ typed_data = {
792
+ "types": {
793
+ "EIP712Domain": [
794
+ {"name": "name", "type": "string"},
795
+ {"name": "version", "type": "string"},
796
+ {"name": "chainId", "type": "uint256"},
797
+ {"name": "verifyingContract", "type": "address"},
798
+ ],
799
+ "Order": [
800
+ {"name": "salt", "type": "uint256"},
801
+ {"name": "maker", "type": "address"},
802
+ {"name": "signer", "type": "address"},
803
+ {"name": "taker", "type": "address"},
804
+ {"name": "tokenId", "type": "uint256"},
805
+ {"name": "makerAmount", "type": "uint256"},
806
+ {"name": "takerAmount", "type": "uint256"},
807
+ {"name": "expiration", "type": "uint256"},
808
+ {"name": "nonce", "type": "uint256"},
809
+ {"name": "feeRateBps", "type": "uint256"},
810
+ {"name": "side", "type": "uint8"},
811
+ {"name": "signatureType", "type": "uint8"},
812
+ ],
813
+ },
814
+ "domain": {
815
+ "name": "Polymarket CTF Exchange",
816
+ "version": "1",
817
+ "chainId": int(chain_id),
818
+ "verifyingContract": exchange_addr,
819
+ },
820
+ "primaryType": "Order",
821
+ "message": message,
822
+ }
823
+
824
+ encoded = encode_typed_data(full_message=typed_data)
825
+ signature = Account.sign_message(encoded, private_key).signature.hex()
826
+ if not signature.startswith("0x"):
827
+ signature = f"0x{signature}"
828
+
829
+ # Convert to submission-friendly dict (stringify big ints; side as BUY/SELL)
830
+ out = {
831
+ # keep salt as int to mirror official client
832
+ "salt": int(message["salt"]),
833
+ "maker": message["maker"],
834
+ "signer": message["signer"],
835
+ "taker": message["taker"],
836
+ "tokenId": str(message["tokenId"]),
837
+ "makerAmount": str(message["makerAmount"]),
838
+ "takerAmount": str(message["takerAmount"]),
839
+ "expiration": str(message["expiration"]),
840
+ "nonce": str(message["nonce"]),
841
+ "feeRateBps": str(message["feeRateBps"]),
842
+ "side": "BUY" if side == 0 else "SELL",
843
+ "signatureType": int(signature_type),
844
+ "signature": signature,
845
+ }
846
+ return out
847
+
848
+ @staticmethod
849
+ def sign_polymarket_order2(
850
+ *,
851
+ private_key: str,
852
+ chain_id: int,
853
+ exchange_address: str,
854
+ order: dict[str, Any],
855
+ ) -> dict[str, Any]:
856
+ """
857
+ Fast EIP-712 signer using coincurve + manual hashing.
858
+
859
+ Avoids heavy typed-data helpers by caching type/domain hashes and
860
+ signing the final digest directly.
861
+ """
862
+ try:
863
+ from coincurve import PrivateKey
864
+ except Exception as e: # pragma: no cover - optional dependency
865
+ raise RuntimeError("coincurve is required for sign_polymarket_order2") from e
866
+
867
+ try:
868
+ now_ts = datetime.now().replace(tzinfo=timezone.utc).timestamp()
869
+ generated_salt = round(now_ts * random())
870
+ except Exception:
871
+ generated_salt = int(time.time())
872
+
873
+ side = int(order.get("side", 0))
874
+ signature_type = int(order.get("signatureType", 1))
875
+ signer_addr = order.get("signer") or Account.from_key(private_key).address
876
+ taker_addr = order.get("taker") or "0x0000000000000000000000000000000000000000"
877
+
878
+ message = {
879
+ "salt": int(order.get("salt") or generated_salt),
880
+ "maker": order.get("maker"),
881
+ "signer": signer_addr,
882
+ "taker": taker_addr,
883
+ "tokenId": int(order["tokenId"]),
884
+ "makerAmount": int(order["makerAmount"]),
885
+ "takerAmount": int(order["takerAmount"]),
886
+ "expiration": int(order.get("expiration", 0)),
887
+ "nonce": int(order.get("nonce", 0)),
888
+ "feeRateBps": int(order.get("feeRateBps", 0)),
889
+ "side": side,
890
+ "signatureType": signature_type,
891
+ }
892
+
893
+ # Normalize addresses for hashing
894
+ exchange_addr = to_checksum_address(exchange_address)
895
+ message["maker"] = to_checksum_address(message["maker"])
896
+ message["signer"] = to_checksum_address(message["signer"])
897
+ message["taker"] = to_checksum_address(message["taker"])
898
+
899
+ domain_sep = _pm_domain_separator(int(chain_id), exchange_addr)
900
+ msg_hash = _pm_order_hash(message)
901
+ typed_hash = keccak(b"\x19\x01" + domain_sep + msg_hash)
902
+
903
+ pk_bytes = bytes.fromhex(private_key[2:] if private_key.startswith("0x") else private_key)
904
+ sig65 = PrivateKey(pk_bytes).sign_recoverable(typed_hash, hasher=None)
905
+ r, s, rec_id = sig65[:32], sig65[32:64], sig65[64]
906
+ v = rec_id + 27 # align with eth_account v
907
+ signature = "0x" + (r + s + bytes([v])).hex()
908
+
909
+ return {
910
+ "salt": int(message["salt"]),
911
+ "maker": message["maker"],
912
+ "signer": message["signer"],
913
+ "taker": message["taker"],
914
+ "tokenId": str(message["tokenId"]),
915
+ "makerAmount": str(message["makerAmount"]),
916
+ "takerAmount": str(message["takerAmount"]),
917
+ "expiration": str(message["expiration"]),
918
+ "nonce": str(message["nonce"]),
919
+ "feeRateBps": str(message["feeRateBps"]),
920
+ "side": "BUY" if side == 0 else "SELL",
921
+ "signatureType": int(signature_type),
922
+ "signature": signature,
923
+ }
924
+
925
+ pybotters.auth.Hosts.items["futures.ourbit.com"] = pybotters.auth.Item(
926
+ "ourbit", Auth.ourbit
927
+ )
928
+ pybotters.auth.Hosts.items["www.ourbit.com"] = pybotters.auth.Item(
929
+ "ourbit", Auth.ourbit_spot
930
+ )
931
+
932
+ pybotters.auth.Hosts.items["www.ourbit.com"] = pybotters.auth.Item(
933
+ "ourbit", Auth.ourbit_spot
934
+ )
935
+
936
+ pybotters.auth.Hosts.items["pro.edgex.exchange"] = pybotters.auth.Item(
937
+ "edgex", Auth.edgex
938
+ )
939
+
940
+
941
+ pybotters.auth.Hosts.items["quote.edgex.exchange"] = pybotters.auth.Item(
942
+ "edgex", Auth.edgex
943
+ )
944
+
945
+ pybotters.auth.Hosts.items["uuapi.rerrkvifj.com"] = pybotters.auth.Item(
946
+ "lbank", Auth.lbank
947
+ )
948
+
949
+ pybotters.auth.Hosts.items["api.coinw.com"] = pybotters.auth.Item(
950
+ "coinw", Auth.coinw
951
+ )
952
+
953
+ pybotters.auth.Hosts.items["derivatives.bitmart.com"] = pybotters.auth.Item(
954
+ "bitmart", Auth.bitmart
955
+ )
956
+
957
+ pybotters.auth.Hosts.items["api-cloud-v2.bitmart.com"] = pybotters.auth.Item(
958
+ "bitmart_api", Auth.bitmart_api
959
+ )
960
+
961
+ pybotters.auth.Hosts.items["api.deepcoin.com"] = pybotters.auth.Item(
962
+ "deepcoin", Auth.deepcoin
963
+ )
964
+ pybotters.auth.Hosts.items["www.deepcoin.com"] = pybotters.auth.Item(
965
+ "deepcoin", Auth.deepcoin
966
+ )
967
+ pybotters.auth.Hosts.items["clob.polymarket.com"] = pybotters.auth.Item(
968
+ "polymarket", Auth.polymarket
969
+ )
970
+ pybotters.auth.Hosts.items["qa-clob.polymarket.com"] = pybotters.auth.Item(
971
+ "polymarket", Auth.polymarket
972
+ )