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.
- {crypto_ws_api-2.0.19 → crypto_ws_api-2.0.20}/CHANGELOG.md +8 -0
- {crypto_ws_api-2.0.19 → crypto_ws_api-2.0.20}/PKG-INFO +4 -3
- {crypto_ws_api-2.0.19 → crypto_ws_api-2.0.20}/README.md +1 -1
- {crypto_ws_api-2.0.19 → crypto_ws_api-2.0.20}/crypto_ws_api/__init__.py +1 -1
- {crypto_ws_api-2.0.19 → crypto_ws_api-2.0.20}/crypto_ws_api/demo.py +0 -1
- {crypto_ws_api-2.0.19 → crypto_ws_api-2.0.20}/crypto_ws_api/ws_api.toml.template +9 -3
- {crypto_ws_api-2.0.19 → crypto_ws_api-2.0.20}/crypto_ws_api/ws_session.py +56 -47
- {crypto_ws_api-2.0.19 → crypto_ws_api-2.0.20}/pyproject.toml +3 -2
- {crypto_ws_api-2.0.19 → crypto_ws_api-2.0.20}/requirements.txt +2 -1
- {crypto_ws_api-2.0.19 → crypto_ws_api-2.0.20}/.deepsource.toml +0 -0
- {crypto_ws_api-2.0.19 → crypto_ws_api-2.0.20}/LICENSE.md +0 -0
|
@@ -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.
|
|
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
|
|
@@ -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.
|
|
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()
|
|
@@ -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 == '
|
|
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-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
params
|
|
287
|
-
|
|
288
|
-
req
|
|
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')
|
|
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}
|
|
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.
|
|
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]
|
|
File without changes
|
|
File without changes
|