crypto-ws-api 2.0.18__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,15 @@
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
+
9
+ ## 2.0.19 - 2025-03-28
10
+ ### Fix
11
+ * `ws_session.ws_login`: Incorrect exit from authentication procedure in case of unsuccessful attempt
12
+
1
13
  ## 2.0.18 - 2025-03-10
2
14
  ### Fix
3
15
  * [🔒] fix/ws_session.py: Proper restoration of WebSocket connection during recurring failures
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: crypto-ws-api
3
- Version: 2.0.18
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
@@ -14,8 +14,9 @@ Classifier: Operating System :: MacOS
14
14
  Requires-Dist: shortuuid~=1.0.13
15
15
  Requires-Dist: platformdirs==4.3.6
16
16
  Requires-Dist: toml~=0.10.2
17
- Requires-Dist: websockets==15.0
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.18"
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()
@@ -200,13 +219,9 @@ class UserWSS:
200
219
  res = await self.request(CONST_WS_START, send_api_key=True)
201
220
  if res is None:
202
221
  logger.warning(f"UserWSS: Not 'logged in' for {self.ws_id}")
203
- if self._ws:
204
- await self._ws.close(code=4000)
222
+ await self.stop()
205
223
  else:
206
- if self.exchange == 'binance':
207
- self._listen_key = res.get('listenKey')
208
- self.tasks_manage(self.heartbeat(), f"heartbeat-{self.ws_id}")
209
- elif self.exchange == 'huobi':
224
+ if self.exchange == 'huobi':
210
225
  self.tasks_manage(self.htx_keepalive(), f"htx_keepalive-{self.ws_id}")
211
226
 
212
227
  self.operational_status = True
@@ -217,7 +232,7 @@ class UserWSS:
217
232
  async def request(self, method, _params=None, send_api_key=False, _signed=False):
218
233
  """
219
234
  Construct and handling request/response to WS API endpoint, use a description of the methods on
220
- https://developers.binance.com/docs/binance-trading-api/websocket_api#request-format
235
+ https://developers.binance.com/docs/binance-spot-api-docs/websocket-api
221
236
  :return: result: {} or None if temporary Out-of-Service state
222
237
  """
223
238
  if self.request_limit_reached:
@@ -244,8 +259,9 @@ class UserWSS:
244
259
  except asyncio.exceptions.TimeoutError:
245
260
  logger.warning(f"UserWSS: get response timeout error: {self.ws_id}")
246
261
  await self.stop()
262
+ return None
247
263
  except asyncio.CancelledError:
248
- pass # Task cancellation should not be logged as an error
264
+ return None
249
265
  else:
250
266
  return res
251
267
 
@@ -257,10 +273,11 @@ class UserWSS:
257
273
  return self._response_pool.pop(_id)
258
274
  elif f"NoneResponse{self.ws_id}" in self._response_pool:
259
275
  return self._response_pool.pop(f"NoneResponse{self.ws_id}", None)
276
+ return None
260
277
 
261
278
  def compose_request(self, _id, send_api_key, method, params, signed):
262
279
  if self.exchange == "binance":
263
- return self._compose_binance_request(_id, send_api_key, method, params, signed)
280
+ return self._compose_binance_request(_id, method, params, signed)
264
281
  elif self.exchange == "okx":
265
282
  return self._compose_okx_request(_id, method, params)
266
283
  elif self.exchange == "bitfinex":
@@ -276,18 +293,17 @@ class UserWSS:
276
293
  else:
277
294
  return {"cid": _id, "ch": method, "params": params}
278
295
 
279
- def _compose_binance_request(self, _id, send_api_key, method, params, signed):
280
- req = {"id": _id, "method": method}
281
- params = params or {}
282
- if send_api_key:
283
- params["apiKey"] = self._api_key
284
- if signed:
285
- params["timestamp"] = int(time.time() * 1000)
286
- payload = '&'.join(f"{key}={value}" for key, value in sorted(params.items()))
287
- params["signature"] = generate_signature('binance_ws', self._api_secret, payload)
288
- if params:
289
- req["params"] = params
290
- 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
291
307
 
292
308
  def _compose_okx_request(self, _id, method, params):
293
309
  if method == CONST_WS_START:
@@ -330,18 +346,6 @@ class UserWSS:
330
346
  logger.info(f"UserWSS order handling status restored for {self.ws_id}")
331
347
  await asyncio.sleep(interval)
332
348
 
333
- async def heartbeat(self, interval=60 * 30):
334
- params = {
335
- "listenKey": self._listen_key,
336
- }
337
- while self.operational_status is not None:
338
- await self.request(
339
- "userDataStream.ping",
340
- params,
341
- send_api_key=True,
342
- )
343
- await asyncio.sleep(interval)
344
-
345
349
  async def htx_keepalive(self, interval=60):
346
350
  await asyncio.sleep(interval * 10)
347
351
  while True:
@@ -475,13 +479,13 @@ class UserWSS:
475
479
  async def binance_error_handle(self, msg):
476
480
  error_msg = msg.get('error')
477
481
  logger.error(f"Malformed request: status: {error_msg}")
478
- if msg.get('status') == 403:
482
+ if msg.get('status') in (401, 403):
479
483
  await self.stop()
480
484
  if msg.get('status') in (418, 429):
481
485
  self._retry_after = error_msg.get('data', {}).get('retryAfter', int((time.time() + TIMEOUT) * 1000))
482
486
  self.request_limit_reached = True
483
487
 
484
- def _handle_rate_limits(self, rate_limits: []):
488
+ def _handle_rate_limits(self, rate_limits: list):
485
489
  def retry_after():
486
490
  return (int(time.time() / interval) + 1) * interval * 1000
487
491
  for rl in rate_limits:
@@ -526,7 +530,11 @@ class UserWSSession:
526
530
  _signed=False,
527
531
  ):
528
532
 
529
- 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}"
530
538
 
531
539
  if ws_id in self.user_wss:
532
540
  user_wss = self.user_wss[ws_id]
@@ -553,6 +561,7 @@ class UserWSSession:
553
561
  while not (user_wss.operational_status and user_wss.order_handling):
554
562
  await asyncio.sleep(DELAY)
555
563
  if duration > TIMEOUT:
564
+ logger.warning(f"{trade_id}: Register timeout for method '{method}'")
556
565
  return None
557
566
  duration += DELAY
558
567
 
@@ -562,6 +571,7 @@ class UserWSSession:
562
571
  pass # Task cancellation should not be logged as an error
563
572
  except Exception as ex:
564
573
  logger.error(f"crypto_ws_api.ws_session.handle_request(): {ex}")
574
+ logger.warning(f"{trade_id}: {method}: None response")
565
575
  return None
566
576
 
567
577
  async def stop(self):
@@ -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
- "websockets==15.0",
24
- "ujson~=5.10.0"
23
+ "websockets==15.0.1",
24
+ "ujson~=5.10.0",
25
+ "cryptography~=44.0.2"
25
26
  ]
26
27
 
27
28
  [tool.flit.module]
@@ -0,0 +1,6 @@
1
+ shortuuid~=1.0.13
2
+ platformdirs==4.3.6
3
+ toml~=0.10.2
4
+ websockets==15.0.1
5
+ ujson~=5.10.0
6
+ cryptography~=44.0.2
@@ -1,5 +0,0 @@
1
- shortuuid~=1.0.13
2
- platformdirs==4.3.6
3
- toml~=0.10.2
4
- websockets==15.0
5
- ujson~=5.10.0