crypto-ws-api 2.0.19__tar.gz → 2.0.20__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.
@@ -1,3 +1,11 @@
1
+ ## 2.0.20 - 2025-04-24
2
+ * On `Binance` now API key type [Ed25519](https://www.binance.com/en/support/faq/detail/6b9a63f1e3384cf48a2eedb82767a69a) is used instead of `HMAC`
3
+ ✨ feat(ws_session): implement `compose_binance_ws_auth` for Binance WebSocket authentication
4
+ 🔧 fix(ws_session): update `generate_signature` for improved signature handling
5
+ 🔧 fix(ws_session): handle `None` return in `request` method
6
+ 🚀 chore(requirements): add `cryptography` dependency for signature handling
7
+ 🔧 fix(pyproject): require Python version 3.10 or higher
8
+
1
9
  ## 2.0.19 - 2025-03-28
2
10
  ### Fix
3
11
  * `ws_session.ws_login`: Incorrect exit from authentication procedure in case of unsuccessful attempt
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: crypto-ws-api
3
- Version: 2.0.19
3
+ Version: 2.0.20
4
4
  Summary: Crypto WS API connector for ASYNC requests
5
5
  Author-email: Jerry Fedorenko <jerry.fedorenko@yahoo.com>
6
- Requires-Python: >=3.8
6
+ Requires-Python: >=3.10
7
7
  Description-Content-Type: text/markdown
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Development Status :: 5 - Production/Stable
@@ -16,6 +16,7 @@ Requires-Dist: platformdirs==4.3.6
16
16
  Requires-Dist: toml~=0.10.2
17
17
  Requires-Dist: websockets==15.0.1
18
18
  Requires-Dist: ujson~=5.10.0
19
+ Requires-Dist: cryptography~=44.0.2
19
20
  Project-URL: Source, https://github.com/DogsTailFarmer/crypto-ws-api
20
21
 
21
22
  <h1 align="center"><img align="center" src="https://user-images.githubusercontent.com/77513676/250364389-cbedc171-a930-4467-a0cd-21627a6a41ed.svg" width="75">Crypto WS API connector for ASYNC requests</h1>
@@ -32,7 +33,7 @@ Project-URL: Source, https://github.com/DogsTailFarmer/crypto-ws-api
32
33
  <a href="https://sonarcloud.io/summary/new_code?id=DogsTailFarmer_crypto-ws-api" target="_blank"><img alt="sonarcloud" title="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=DogsTailFarmer_crypto-ws-api&metric=alert_status"/></a>
33
34
  <a href="https://pepy.tech/project/crypto-ws-api" target="_blank"><img alt="Downloads" title="Downloads" src="https://static.pepy.tech/badge/crypto-ws-api"/></a>
34
35
  ***
35
- For :heavy_check_mark:Binance, :heavy_check_mark:OKX, :heavy_check_mark:Bitfinex, :heavy_check_mark:HTX
36
+ For :heavy_check_mark:[Binance](https://accounts.binance.com/en/register?ref=FXQ6HY5O), :heavy_check_mark:[OKX](https://okx.com/join/2607649), :heavy_check_mark:[Bitfinex](https://www.bitfinex.com/sign-up?refcode=v_4az2nCP), :heavy_check_mark:[HTX](https://www.htx.com/invite/en-us/1f?invite_code=9uaw3223)
36
37
  ***
37
38
 
38
39
  ## Features
@@ -12,7 +12,7 @@
12
12
  <a href="https://sonarcloud.io/summary/new_code?id=DogsTailFarmer_crypto-ws-api" target="_blank"><img alt="sonarcloud" title="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=DogsTailFarmer_crypto-ws-api&metric=alert_status"/></a>
13
13
  <a href="https://pepy.tech/project/crypto-ws-api" target="_blank"><img alt="Downloads" title="Downloads" src="https://static.pepy.tech/badge/crypto-ws-api"/></a>
14
14
  ***
15
- For :heavy_check_mark:Binance, :heavy_check_mark:OKX, :heavy_check_mark:Bitfinex, :heavy_check_mark:HTX
15
+ For :heavy_check_mark:[Binance](https://accounts.binance.com/en/register?ref=FXQ6HY5O), :heavy_check_mark:[OKX](https://okx.com/join/2607649), :heavy_check_mark:[Bitfinex](https://www.bitfinex.com/sign-up?refcode=v_4az2nCP), :heavy_check_mark:[HTX](https://www.htx.com/invite/en-us/1f?invite_code=9uaw3223)
16
16
  ***
17
17
 
18
18
  ## Features
@@ -13,7 +13,7 @@ __maintainer__ = "Jerry Fedorenko"
13
13
  __contact__ = "https://github.com/DogsTailFarmer"
14
14
  __email__ = "jerry.fedorenko@yahoo.com"
15
15
  __credits__ = ["https://github.com/DanyaSWorlD"]
16
- __version__ = "2.0.19"
16
+ __version__ = "2.0.20"
17
17
 
18
18
  from pathlib import Path
19
19
  import shutil
@@ -101,7 +101,6 @@ async def account_information(user_session: UserWSSession, _trade_id):
101
101
  res = await user_session.handle_request(
102
102
  _trade_id,
103
103
  "account.status",
104
- send_api_key=True,
105
104
  _signed=True
106
105
  )
107
106
  if res is None:
@@ -1,6 +1,6 @@
1
1
  # Parameters for crypto-ws-api connector
2
- # Copyright © 2023 Jerry Fedorenko aka VM
3
- # __version__ = "1.0.0"
2
+ # Copyright © 2023-2025 Jerry Fedorenko aka VM
3
+ # __version__ = "2.0.20"
4
4
 
5
5
  # region endpoint
6
6
  [endpoint]
@@ -10,10 +10,16 @@
10
10
  # endregion
11
11
 
12
12
  # region Binance accounts
13
+ # How to Generate an Ed25519 Key Pair and Register then on Binance:
14
+ # https://www.binance.com/en/support/faq/detail/6b9a63f1e3384cf48a2eedb82767a69a
13
15
  [[accounts]]
14
16
  exchange = 'binance'
15
17
  name = 'Demo - Binance'
16
18
  api_key = '*********** Place API key there ************'
17
- api_secret = '*********** Place secret API key there ************'
19
+ api_secret = """
20
+ -----BEGIN PRIVATE KEY-----
21
+ *********** Place Private Ed25519 key there ************
22
+ -----END PRIVATE KEY-----
23
+ """
18
24
  test_net = true
19
25
  # endregion
@@ -15,6 +15,7 @@ import string
15
15
  import random
16
16
  from datetime import datetime, timezone
17
17
  from urllib.parse import urlencode, urlparse
18
+ from cryptography.hazmat.primitives.serialization import load_pem_private_key
18
19
 
19
20
  from websockets.asyncio.client import connect
20
21
  from websockets import ConnectionClosed
@@ -49,20 +50,40 @@ def set_logger(name, log_file, file_level=logging.INFO, propagate=False):
49
50
 
50
51
  logger = set_logger(__name__, Path(LOG_PATH, 'ws_session.log'))
51
52
 
53
+
52
54
  def generate_signature(exchange: str, secret: str, data: str) -> str:
53
- digest_func = hashlib.sha384 if exchange == 'bitfinex' else hashlib.sha256
54
- encoding = "ascii" if exchange == 'binance_ws' else "utf-8"
55
- key = secret.encode(encoding=encoding)
56
- signature = hmac.new(
57
- bytes(key) if exchange == 'bybit' else key,
58
- data.encode(encoding=encoding),
59
- digest_func
60
- )
55
+ encoding = "utf-8"
56
+ if exchange == 'binance':
57
+ signature = load_pem_private_key(
58
+ data=bytes(secret, encoding=encoding), password=None).sign(data.encode("ascii")
59
+ )
60
+ else:
61
+ digest_func = hashlib.sha384 if exchange == 'bitfinex' else hashlib.sha256
62
+ key = secret.encode(encoding=encoding)
63
+ signature = hmac.new(
64
+ bytes(key) if exchange == 'bybit' else key,
65
+ data.encode(encoding=encoding),
66
+ digest_func
67
+ )
61
68
  if exchange in {"huobi", "okx"}:
62
- return base64.b64encode(signature.digest()).decode()
69
+ signature = signature.digest()
70
+
71
+ if exchange in {"binance", "huobi", "okx"}:
72
+ return base64.b64encode(signature).decode()
63
73
  else:
64
74
  return signature.hexdigest()
65
75
 
76
+ def compose_binance_ws_auth(_id: str, api_key: str, api_secret: str) -> dict:
77
+ req = {"id": _id, "method": "session.logon"}
78
+ params = {
79
+ "apiKey": api_key,
80
+ "timestamp": int(time.time() * 1000)
81
+ }
82
+ payload = '&'.join(f"{key}={value}" for key, value in sorted(params.items()))
83
+ params["signature"] = generate_signature('binance', api_secret, payload)
84
+ req["params"] = params
85
+ return req
86
+
66
87
  def compose_htx_ws_auth(endpoint, exchange, api_key, api_secret):
67
88
  _params = {
68
89
  "accessKey": api_key,
@@ -98,7 +119,6 @@ class UserWSS:
98
119
  "_api_secret",
99
120
  "_passphrase",
100
121
  "_ws",
101
- "_listen_key",
102
122
  "_retry_after",
103
123
  "ws_id",
104
124
  "operational_status",
@@ -120,7 +140,6 @@ class UserWSS:
120
140
  self._api_secret = api_secret
121
141
  self._passphrase = passphrase
122
142
  self._ws = None
123
- self._listen_key = None
124
143
  self._response_pool = {}
125
144
  self._retry_after = int(time.time() * 1000) - 1
126
145
  self.ws_id = ws_id
@@ -148,7 +167,7 @@ class UserWSS:
148
167
  async def _ws_listener(self):
149
168
  self.tasks_manage(self.ws_login())
150
169
  async for msg in self._ws:
151
- # logger.info(f"_ws_listener: msg: {self.ws_id}: {msg}")
170
+ # logger.info(f"_ws_listener: ws_id: {self.ws_id} msg: {msg}")
152
171
  if isinstance(msg, str):
153
172
  try:
154
173
  _msg = json.loads(msg)
@@ -179,7 +198,7 @@ class UserWSS:
179
198
  async for self._ws in connect(
180
199
  self.endpoint,
181
200
  logger=self.logger,
182
- ping_interval=None if self.exchange == 'huobi' else 20
201
+ ping_interval=None if self.exchange in ('binance', 'huobi') else 20
183
202
  ):
184
203
  try:
185
204
  await self._ws_listener()
@@ -202,10 +221,7 @@ class UserWSS:
202
221
  logger.warning(f"UserWSS: Not 'logged in' for {self.ws_id}")
203
222
  await self.stop()
204
223
  else:
205
- if self.exchange == 'binance':
206
- self._listen_key = res.get('listenKey')
207
- self.tasks_manage(self.heartbeat(), f"heartbeat-{self.ws_id}")
208
- elif self.exchange == 'huobi':
224
+ if self.exchange == 'huobi':
209
225
  self.tasks_manage(self.htx_keepalive(), f"htx_keepalive-{self.ws_id}")
210
226
 
211
227
  self.operational_status = True
@@ -216,7 +232,7 @@ class UserWSS:
216
232
  async def request(self, method, _params=None, send_api_key=False, _signed=False):
217
233
  """
218
234
  Construct and handling request/response to WS API endpoint, use a description of the methods on
219
- https://developers.binance.com/docs/binance-trading-api/websocket_api#request-format
235
+ https://developers.binance.com/docs/binance-spot-api-docs/websocket-api
220
236
  :return: result: {} or None if temporary Out-of-Service state
221
237
  """
222
238
  if self.request_limit_reached:
@@ -243,8 +259,9 @@ class UserWSS:
243
259
  except asyncio.exceptions.TimeoutError:
244
260
  logger.warning(f"UserWSS: get response timeout error: {self.ws_id}")
245
261
  await self.stop()
262
+ return None
246
263
  except asyncio.CancelledError:
247
- pass # Task cancellation should not be logged as an error
264
+ return None
248
265
  else:
249
266
  return res
250
267
 
@@ -256,10 +273,11 @@ class UserWSS:
256
273
  return self._response_pool.pop(_id)
257
274
  elif f"NoneResponse{self.ws_id}" in self._response_pool:
258
275
  return self._response_pool.pop(f"NoneResponse{self.ws_id}", None)
276
+ return None
259
277
 
260
278
  def compose_request(self, _id, send_api_key, method, params, signed):
261
279
  if self.exchange == "binance":
262
- return self._compose_binance_request(_id, send_api_key, method, params, signed)
280
+ return self._compose_binance_request(_id, method, params, signed)
263
281
  elif self.exchange == "okx":
264
282
  return self._compose_okx_request(_id, method, params)
265
283
  elif self.exchange == "bitfinex":
@@ -275,18 +293,17 @@ class UserWSS:
275
293
  else:
276
294
  return {"cid": _id, "ch": method, "params": params}
277
295
 
278
- def _compose_binance_request(self, _id, send_api_key, method, params, signed):
279
- req = {"id": _id, "method": method}
280
- params = params or {}
281
- if send_api_key:
282
- params["apiKey"] = self._api_key
283
- if signed:
284
- params["timestamp"] = int(time.time() * 1000)
285
- payload = '&'.join(f"{key}={value}" for key, value in sorted(params.items()))
286
- params["signature"] = generate_signature('binance_ws', self._api_secret, payload)
287
- if params:
288
- req["params"] = params
289
- return req
296
+ def _compose_binance_request(self, _id, method, params, signed):
297
+ if method == CONST_WS_START:
298
+ return compose_binance_ws_auth(_id, self._api_key, self._api_secret)
299
+ else:
300
+ req = {"id": _id, "method": method}
301
+ params = params or {}
302
+ if signed:
303
+ params["timestamp"] = int(time.time() * 1000)
304
+ if params:
305
+ req["params"] = params
306
+ return req
290
307
 
291
308
  def _compose_okx_request(self, _id, method, params):
292
309
  if method == CONST_WS_START:
@@ -329,18 +346,6 @@ class UserWSS:
329
346
  logger.info(f"UserWSS order handling status restored for {self.ws_id}")
330
347
  await asyncio.sleep(interval)
331
348
 
332
- async def heartbeat(self, interval=60 * 30):
333
- params = {
334
- "listenKey": self._listen_key,
335
- }
336
- while self.operational_status is not None:
337
- await self.request(
338
- "userDataStream.ping",
339
- params,
340
- send_api_key=True,
341
- )
342
- await asyncio.sleep(interval)
343
-
344
349
  async def htx_keepalive(self, interval=60):
345
350
  await asyncio.sleep(interval * 10)
346
351
  while True:
@@ -474,13 +479,13 @@ class UserWSS:
474
479
  async def binance_error_handle(self, msg):
475
480
  error_msg = msg.get('error')
476
481
  logger.error(f"Malformed request: status: {error_msg}")
477
- if msg.get('status') == 403:
482
+ if msg.get('status') in (401, 403):
478
483
  await self.stop()
479
484
  if msg.get('status') in (418, 429):
480
485
  self._retry_after = error_msg.get('data', {}).get('retryAfter', int((time.time() + TIMEOUT) * 1000))
481
486
  self.request_limit_reached = True
482
487
 
483
- def _handle_rate_limits(self, rate_limits: []):
488
+ def _handle_rate_limits(self, rate_limits: list):
484
489
  def retry_after():
485
490
  return (int(time.time() / interval) + 1) * interval * 1000
486
491
  for rl in rate_limits:
@@ -525,7 +530,11 @@ class UserWSSession:
525
530
  _signed=False,
526
531
  ):
527
532
 
528
- ws_id = f"{self.exchange}-{trade_id}-{method}"
533
+ ws_id = f"{self.exchange}-{trade_id}"
534
+ if self.exchange in ('binance', 'vertex'):
535
+ ws_id = f"{ws_id}{'_ST' if _signed else '_SF'}"
536
+ else:
537
+ ws_id = f"{ws_id}-{method}"
529
538
 
530
539
  if ws_id in self.user_wss:
531
540
  user_wss = self.user_wss[ws_id]
@@ -14,14 +14,15 @@ classifiers=["Programming Language :: Python :: 3",
14
14
  "Operating System :: Microsoft :: Windows",
15
15
  "Operating System :: MacOS"]
16
16
  dynamic = ["version", "description"]
17
- requires-python = ">=3.8"
17
+ requires-python = ">=3.10"
18
18
 
19
19
  dependencies = [
20
20
  "shortuuid~=1.0.13",
21
21
  "platformdirs==4.3.6",
22
22
  "toml~=0.10.2",
23
23
  "websockets==15.0.1",
24
- "ujson~=5.10.0"
24
+ "ujson~=5.10.0",
25
+ "cryptography~=44.0.2"
25
26
  ]
26
27
 
27
28
  [tool.flit.module]
@@ -2,4 +2,5 @@ shortuuid~=1.0.13
2
2
  platformdirs==4.3.6
3
3
  toml~=0.10.2
4
4
  websockets==15.0.1
5
- ujson~=5.10.0
5
+ ujson~=5.10.0
6
+ cryptography~=44.0.2