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.
- hyperquant/__init__.py +8 -0
- hyperquant/broker/auth.py +972 -0
- hyperquant/broker/bitget.py +311 -0
- hyperquant/broker/bitmart.py +720 -0
- hyperquant/broker/coinw.py +487 -0
- hyperquant/broker/deepcoin.py +651 -0
- hyperquant/broker/edgex.py +500 -0
- hyperquant/broker/hyperliquid.py +570 -0
- hyperquant/broker/lbank.py +661 -0
- hyperquant/broker/lib/edgex_sign.py +455 -0
- hyperquant/broker/lib/hpstore.py +252 -0
- hyperquant/broker/lib/hyper_types.py +48 -0
- hyperquant/broker/lib/polymarket/ctfAbi.py +721 -0
- hyperquant/broker/lib/polymarket/safeAbi.py +1138 -0
- hyperquant/broker/lib/util.py +22 -0
- hyperquant/broker/lighter.py +679 -0
- hyperquant/broker/models/apexpro.py +150 -0
- hyperquant/broker/models/bitget.py +359 -0
- hyperquant/broker/models/bitmart.py +635 -0
- hyperquant/broker/models/coinw.py +724 -0
- hyperquant/broker/models/deepcoin.py +809 -0
- hyperquant/broker/models/edgex.py +1053 -0
- hyperquant/broker/models/hyperliquid.py +284 -0
- hyperquant/broker/models/lbank.py +557 -0
- hyperquant/broker/models/lighter.py +868 -0
- hyperquant/broker/models/ourbit.py +1155 -0
- hyperquant/broker/models/polymarket.py +1071 -0
- hyperquant/broker/ourbit.py +550 -0
- hyperquant/broker/polymarket.py +2399 -0
- hyperquant/broker/ws.py +132 -0
- hyperquant/core.py +513 -0
- hyperquant/datavison/_util.py +18 -0
- hyperquant/datavison/binance.py +111 -0
- hyperquant/datavison/coinglass.py +237 -0
- hyperquant/datavison/okx.py +177 -0
- hyperquant/db.py +191 -0
- hyperquant/draw.py +1200 -0
- hyperquant/logkit.py +205 -0
- hyperquant/notikit.py +124 -0
- hyperquant-1.48.dist-info/METADATA +32 -0
- hyperquant-1.48.dist-info/RECORD +42 -0
- 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
|
+
)
|