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.
- {crypto_ws_api-2.0.18 → crypto_ws_api-2.0.20}/CHANGELOG.md +12 -0
- {crypto_ws_api-2.0.18 → crypto_ws_api-2.0.20}/PKG-INFO +5 -4
- {crypto_ws_api-2.0.18 → crypto_ws_api-2.0.20}/README.md +1 -1
- {crypto_ws_api-2.0.18 → crypto_ws_api-2.0.20}/crypto_ws_api/__init__.py +1 -1
- {crypto_ws_api-2.0.18 → crypto_ws_api-2.0.20}/crypto_ws_api/demo.py +0 -1
- {crypto_ws_api-2.0.18 → crypto_ws_api-2.0.20}/crypto_ws_api/ws_api.toml.template +9 -3
- {crypto_ws_api-2.0.18 → crypto_ws_api-2.0.20}/crypto_ws_api/ws_session.py +59 -49
- {crypto_ws_api-2.0.18 → crypto_ws_api-2.0.20}/pyproject.toml +4 -3
- crypto_ws_api-2.0.20/requirements.txt +6 -0
- crypto_ws_api-2.0.18/requirements.txt +0 -5
- {crypto_ws_api-2.0.18 → crypto_ws_api-2.0.20}/.deepsource.toml +0 -0
- {crypto_ws_api-2.0.18 → crypto_ws_api-2.0.20}/LICENSE.md +0 -0
|
@@ -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.
|
|
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.
|
|
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.
|
|
16
|
+
__version__ = "2.0.20"
|
|
17
17
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
import shutil
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Parameters for crypto-ws-api connector
|
|
2
|
-
# Copyright © 2023 Jerry Fedorenko aka VM
|
|
3
|
-
# __version__ = "
|
|
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 =
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
204
|
-
await self._ws.close(code=4000)
|
|
222
|
+
await self.stop()
|
|
205
223
|
else:
|
|
206
|
-
if self.exchange == '
|
|
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-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
params
|
|
288
|
-
|
|
289
|
-
req
|
|
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')
|
|
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}
|
|
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.
|
|
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]
|
|
File without changes
|
|
File without changes
|