tonutils 2.0.1b2__py3-none-any.whl → 2.0.1b4__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.
- tonutils/__init__.py +0 -2
- tonutils/__meta__.py +1 -1
- tonutils/cli.py +111 -0
- tonutils/clients/__init__.py +7 -11
- tonutils/clients/adnl/__init__.py +7 -3
- tonutils/clients/adnl/balancer.py +362 -168
- tonutils/clients/adnl/client.py +203 -67
- tonutils/clients/adnl/provider/config.py +24 -25
- tonutils/clients/adnl/provider/models.py +4 -0
- tonutils/clients/adnl/provider/provider.py +203 -160
- tonutils/clients/adnl/provider/transport.py +44 -33
- tonutils/clients/adnl/provider/workers/base.py +0 -2
- tonutils/clients/adnl/provider/workers/pinger.py +1 -1
- tonutils/clients/adnl/provider/workers/reader.py +3 -2
- tonutils/clients/adnl/{provider/builder.py → utils.py} +62 -2
- tonutils/clients/http/__init__.py +11 -8
- tonutils/clients/http/balancer.py +75 -63
- tonutils/clients/http/clients/__init__.py +13 -0
- tonutils/clients/http/clients/chainstack.py +48 -0
- tonutils/clients/http/clients/quicknode.py +47 -0
- tonutils/clients/http/clients/tatum.py +56 -0
- tonutils/clients/http/{tonapi/client.py → clients/tonapi.py} +31 -31
- tonutils/clients/http/{toncenter/client.py → clients/toncenter.py} +59 -48
- tonutils/clients/http/providers/__init__.py +4 -0
- tonutils/clients/http/providers/base.py +201 -0
- tonutils/clients/http/providers/response.py +85 -0
- tonutils/clients/http/providers/tonapi/__init__.py +3 -0
- tonutils/clients/http/{tonapi → providers/tonapi}/models.py +1 -0
- tonutils/clients/http/providers/tonapi/provider.py +125 -0
- tonutils/clients/http/providers/toncenter/__init__.py +3 -0
- tonutils/clients/http/{toncenter → providers/toncenter}/models.py +1 -0
- tonutils/clients/http/providers/toncenter/provider.py +119 -0
- tonutils/clients/http/utils.py +140 -0
- tonutils/clients/limiter.py +115 -0
- tonutils/contracts/__init__.py +4 -0
- tonutils/contracts/base.py +33 -20
- tonutils/contracts/dns/methods.py +2 -2
- tonutils/contracts/jetton/methods.py +2 -2
- tonutils/contracts/nft/methods.py +2 -2
- tonutils/contracts/nft/tlb.py +1 -1
- tonutils/{protocols/contract.py → contracts/protocol.py} +29 -29
- tonutils/contracts/telegram/methods.py +2 -2
- tonutils/contracts/vanity/vanity.py +1 -1
- tonutils/contracts/wallet/__init__.py +2 -0
- tonutils/contracts/wallet/base.py +3 -3
- tonutils/contracts/wallet/messages.py +1 -1
- tonutils/contracts/wallet/methods.py +2 -2
- tonutils/{protocols/wallet.py → contracts/wallet/protocol.py} +35 -35
- tonutils/contracts/wallet/versions/v5.py +3 -3
- tonutils/exceptions.py +146 -228
- tonutils/tonconnect/__init__.py +0 -0
- tonutils/tools/__init__.py +6 -0
- tonutils/tools/block_scanner/__init__.py +26 -0
- tonutils/tools/block_scanner/annotations.py +23 -0
- tonutils/tools/block_scanner/dispatcher.py +141 -0
- tonutils/tools/block_scanner/events.py +31 -0
- tonutils/tools/block_scanner/scanner.py +315 -0
- tonutils/tools/block_scanner/traversal.py +96 -0
- tonutils/tools/block_scanner/where.py +151 -0
- tonutils/tools/status_monitor/__init__.py +3 -0
- tonutils/tools/status_monitor/console.py +157 -0
- tonutils/tools/status_monitor/models.py +27 -0
- tonutils/tools/status_monitor/monitor.py +295 -0
- tonutils/types.py +125 -2
- tonutils/utils.py +3 -3
- {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b4.dist-info}/METADATA +2 -5
- tonutils-2.0.1b4.dist-info/RECORD +108 -0
- tonutils-2.0.1b4.dist-info/entry_points.txt +2 -0
- tonutils/clients/adnl/provider/limiter.py +0 -56
- tonutils/clients/adnl/stack.py +0 -64
- tonutils/clients/http/chainstack/__init__.py +0 -4
- tonutils/clients/http/chainstack/client.py +0 -63
- tonutils/clients/http/chainstack/provider.py +0 -44
- tonutils/clients/http/quicknode/__init__.py +0 -4
- tonutils/clients/http/quicknode/client.py +0 -60
- tonutils/clients/http/quicknode/provider.py +0 -42
- tonutils/clients/http/tatum/__init__.py +0 -4
- tonutils/clients/http/tatum/client.py +0 -66
- tonutils/clients/http/tatum/provider.py +0 -53
- tonutils/clients/http/tonapi/__init__.py +0 -4
- tonutils/clients/http/tonapi/provider.py +0 -150
- tonutils/clients/http/tonapi/stack.py +0 -71
- tonutils/clients/http/toncenter/__init__.py +0 -4
- tonutils/clients/http/toncenter/provider.py +0 -145
- tonutils/clients/http/toncenter/stack.py +0 -73
- tonutils/protocols/__init__.py +0 -9
- tonutils-2.0.1b2.dist-info/RECORD +0 -98
- /tonutils/{protocols/client.py → clients/protocol.py} +0 -0
- {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b4.dist-info}/WHEEL +0 -0
- {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b4.dist-info}/licenses/LICENSE +0 -0
- {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b4.dist-info}/top_level.txt +0 -0
|
@@ -16,13 +16,7 @@ from pytoniq_core.crypto.ciphers import (
|
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
from tonutils.clients.adnl.provider.models import LiteServer
|
|
19
|
-
from tonutils.exceptions import
|
|
20
|
-
AdnlHandshakeError,
|
|
21
|
-
AdnlTransportCipherError,
|
|
22
|
-
AdnlTransportFrameError,
|
|
23
|
-
AdnlTransportStateError,
|
|
24
|
-
AdnlTransportError,
|
|
25
|
-
)
|
|
19
|
+
from tonutils.exceptions import TransportError, NotConnectedError
|
|
26
20
|
|
|
27
21
|
|
|
28
22
|
class AdnlTcpTransport:
|
|
@@ -40,13 +34,14 @@ class AdnlTcpTransport:
|
|
|
40
34
|
for incoming frames.
|
|
41
35
|
"""
|
|
42
36
|
|
|
43
|
-
def __init__(self, node: LiteServer,
|
|
37
|
+
def __init__(self, node: LiteServer, connect_timeout: float) -> None:
|
|
44
38
|
"""
|
|
45
39
|
Initialize ADNL TCP transport for a liteserver connection.
|
|
46
40
|
|
|
47
41
|
:param node: Liteserver configuration with host, port, and public key
|
|
48
|
-
:param
|
|
42
|
+
:param connect_timeout: Timeout in seconds for connection
|
|
49
43
|
"""
|
|
44
|
+
self.node = node
|
|
50
45
|
self.server = Server(
|
|
51
46
|
host=node.host,
|
|
52
47
|
port=node.port,
|
|
@@ -54,7 +49,7 @@ class AdnlTcpTransport:
|
|
|
54
49
|
)
|
|
55
50
|
self.client = Client(Client.generate_ed25519_private_key())
|
|
56
51
|
|
|
57
|
-
self.
|
|
52
|
+
self.connect_timeout = connect_timeout
|
|
58
53
|
self.enc_cipher = None
|
|
59
54
|
self.dec_cipher = None
|
|
60
55
|
|
|
@@ -73,6 +68,14 @@ class AdnlTcpTransport:
|
|
|
73
68
|
"""Check if the transport is currently connected."""
|
|
74
69
|
return self._connected
|
|
75
70
|
|
|
71
|
+
def _error(self, operation: str, reason: str) -> TransportError:
|
|
72
|
+
"""Create TransportError with endpoint context."""
|
|
73
|
+
return TransportError(
|
|
74
|
+
endpoint=self.node.endpoint,
|
|
75
|
+
operation=operation,
|
|
76
|
+
reason=reason,
|
|
77
|
+
)
|
|
78
|
+
|
|
76
79
|
@staticmethod
|
|
77
80
|
def _build_frame(data: bytes) -> bytes:
|
|
78
81
|
"""
|
|
@@ -131,12 +134,12 @@ class AdnlTcpTransport:
|
|
|
131
134
|
async def _flush(self) -> None:
|
|
132
135
|
"""Flush the TCP write buffer."""
|
|
133
136
|
if self.writer is None:
|
|
134
|
-
raise
|
|
137
|
+
raise self._error("send", "writer not initialized")
|
|
135
138
|
try:
|
|
136
139
|
await self.writer.drain()
|
|
137
|
-
except ConnectionError:
|
|
140
|
+
except ConnectionError as exc:
|
|
138
141
|
await self.close()
|
|
139
|
-
raise
|
|
142
|
+
raise self._error("send", "connection lost") from exc
|
|
140
143
|
|
|
141
144
|
def encrypt_frame(self, data: bytes) -> bytes:
|
|
142
145
|
"""
|
|
@@ -145,7 +148,7 @@ class AdnlTcpTransport:
|
|
|
145
148
|
:param data: Plaintext frame bytes
|
|
146
149
|
"""
|
|
147
150
|
if self.enc_cipher is None:
|
|
148
|
-
raise
|
|
151
|
+
raise self._error("encrypt", "cipher not initialized")
|
|
149
152
|
return aes_ctr_encrypt(self.enc_cipher, data)
|
|
150
153
|
|
|
151
154
|
def decrypt_frame(self, data: bytes) -> bytes:
|
|
@@ -155,22 +158,30 @@ class AdnlTcpTransport:
|
|
|
155
158
|
:param data: Encrypted frame bytes
|
|
156
159
|
"""
|
|
157
160
|
if self.dec_cipher is None:
|
|
158
|
-
raise
|
|
161
|
+
raise self._error("decrypt", "cipher not initialized")
|
|
159
162
|
return aes_ctr_decrypt(self.dec_cipher, data)
|
|
160
163
|
|
|
161
164
|
async def connect(self) -> None:
|
|
162
165
|
"""Establish encrypted connection to the liteserver."""
|
|
163
166
|
if self._connected:
|
|
164
|
-
raise
|
|
167
|
+
raise self._error("connect", "already connected")
|
|
165
168
|
|
|
166
169
|
self.loop = asyncio.get_running_loop()
|
|
167
170
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
try:
|
|
172
|
+
self.reader, self.writer = await asyncio.wait_for(
|
|
173
|
+
asyncio.open_connection(self.server.host, self.server.port),
|
|
174
|
+
timeout=self.connect_timeout,
|
|
175
|
+
)
|
|
176
|
+
except asyncio.TimeoutError as exc:
|
|
177
|
+
raise self._error(
|
|
178
|
+
"connect", f"timeout after {self.connect_timeout}s"
|
|
179
|
+
) from exc
|
|
180
|
+
except OSError as exc:
|
|
181
|
+
raise self._error("connect", "connection refused") from exc
|
|
182
|
+
|
|
172
183
|
if self.writer is None or self.reader is None:
|
|
173
|
-
raise
|
|
184
|
+
raise self._error("connect", "stream init failed")
|
|
174
185
|
|
|
175
186
|
try:
|
|
176
187
|
handshake = self._build_handshake()
|
|
@@ -180,16 +191,15 @@ class AdnlTcpTransport:
|
|
|
180
191
|
try:
|
|
181
192
|
await asyncio.wait_for(
|
|
182
193
|
self.read_frame(discard=True),
|
|
183
|
-
timeout=self.
|
|
194
|
+
timeout=self.connect_timeout,
|
|
184
195
|
)
|
|
185
196
|
except asyncio.IncompleteReadError as exc:
|
|
186
|
-
raise
|
|
187
|
-
"ADNL handshake failed: remote closed connection"
|
|
188
|
-
) from exc
|
|
197
|
+
raise self._error("handshake", "remote closed") from exc
|
|
189
198
|
except asyncio.TimeoutError as exc:
|
|
190
|
-
raise
|
|
191
|
-
f"
|
|
199
|
+
raise self._error(
|
|
200
|
+
"handshake", f"timeout after {self.connect_timeout}s"
|
|
192
201
|
) from exc
|
|
202
|
+
|
|
193
203
|
self._connected = True
|
|
194
204
|
self._reader_task = asyncio.create_task(
|
|
195
205
|
self.frame_reader_loop(),
|
|
@@ -208,7 +218,7 @@ class AdnlTcpTransport:
|
|
|
208
218
|
:param payload: Raw ADNL packet bytes
|
|
209
219
|
"""
|
|
210
220
|
if not self._connected or self.writer is None:
|
|
211
|
-
raise
|
|
221
|
+
raise NotConnectedError()
|
|
212
222
|
|
|
213
223
|
packet = self._build_frame(payload)
|
|
214
224
|
encrypted = self.encrypt_frame(packet)
|
|
@@ -223,7 +233,7 @@ class AdnlTcpTransport:
|
|
|
223
233
|
Blocks until a complete packet is available from the background reader.
|
|
224
234
|
"""
|
|
225
235
|
if not self._connected:
|
|
226
|
-
raise
|
|
236
|
+
raise NotConnectedError()
|
|
227
237
|
return await self._incoming.get()
|
|
228
238
|
|
|
229
239
|
async def read_frame(self, discard: bool = False) -> t.Optional[bytes]:
|
|
@@ -238,24 +248,24 @@ class AdnlTcpTransport:
|
|
|
238
248
|
:param discard: If True, validates but returns None (used for handshake ack)
|
|
239
249
|
"""
|
|
240
250
|
if self.reader is None:
|
|
241
|
-
raise
|
|
251
|
+
raise self._error("recv", "reader not initialized")
|
|
242
252
|
|
|
243
253
|
length_enc = await self.reader.readexactly(4)
|
|
244
254
|
length_dec = self.decrypt_frame(length_enc)
|
|
245
255
|
data_len = int.from_bytes(length_dec, "little")
|
|
246
256
|
|
|
247
257
|
if data_len <= 0:
|
|
248
|
-
raise
|
|
258
|
+
raise self._error("recv", f"invalid frame length: {data_len}")
|
|
249
259
|
|
|
250
260
|
data_enc = await self.reader.readexactly(data_len)
|
|
251
261
|
data = self.decrypt_frame(data_enc)
|
|
252
262
|
|
|
253
263
|
if len(data) < 32:
|
|
254
|
-
raise
|
|
264
|
+
raise self._error("recv", f"frame too short: {len(data)} bytes")
|
|
255
265
|
|
|
256
266
|
payload, checksum = data[:-32], data[-32:]
|
|
257
267
|
if hashlib.sha256(payload).digest() != checksum:
|
|
258
|
-
raise
|
|
268
|
+
raise self._error("recv", "checksum mismatch")
|
|
259
269
|
|
|
260
270
|
if discard:
|
|
261
271
|
return None
|
|
@@ -272,6 +282,7 @@ class AdnlTcpTransport:
|
|
|
272
282
|
except asyncio.CancelledError:
|
|
273
283
|
pass
|
|
274
284
|
except (
|
|
285
|
+
TransportError,
|
|
275
286
|
asyncio.IncompleteReadError,
|
|
276
287
|
ConnectionAbortedError,
|
|
277
288
|
ConnectionError,
|
|
@@ -66,7 +66,7 @@ class PingerWorker(BaseWorker):
|
|
|
66
66
|
await self.provider.transport.send_adnl_packet(payload)
|
|
67
67
|
|
|
68
68
|
start = self.provider.loop.time()
|
|
69
|
-
await asyncio.wait_for(fut, timeout=self.provider.
|
|
69
|
+
await asyncio.wait_for(fut, timeout=self.provider.request_timeout)
|
|
70
70
|
end = self.provider.loop.time()
|
|
71
71
|
|
|
72
72
|
self._last_time = end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from tonutils.clients.adnl.provider.workers.base import BaseWorker
|
|
4
|
-
from tonutils.exceptions import
|
|
4
|
+
from tonutils.exceptions import ProviderResponseError
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class ReaderWorker(BaseWorker):
|
|
@@ -35,9 +35,10 @@ class ReaderWorker(BaseWorker):
|
|
|
35
35
|
payload = root.get("answer", root)
|
|
36
36
|
|
|
37
37
|
if "code" in payload and "message" in payload:
|
|
38
|
-
exception =
|
|
38
|
+
exception = ProviderResponseError(
|
|
39
39
|
code=payload["code"],
|
|
40
40
|
message=payload["message"],
|
|
41
|
+
endpoint=self.provider.node.endpoint,
|
|
41
42
|
)
|
|
42
43
|
fut.set_exception(exception)
|
|
43
44
|
else:
|
|
@@ -11,10 +11,12 @@ from pytoniq_core import (
|
|
|
11
11
|
SimpleAccount,
|
|
12
12
|
begin_cell,
|
|
13
13
|
check_account_proof,
|
|
14
|
+
Slice,
|
|
15
|
+
VmTuple,
|
|
14
16
|
)
|
|
15
17
|
|
|
16
|
-
from tonutils.types import ContractState, ContractStateInfo
|
|
17
|
-
from tonutils.utils import cell_to_hex
|
|
18
|
+
from tonutils.types import ContractState, ContractStateInfo, StackItems, StackItem
|
|
19
|
+
from tonutils.utils import cell_to_hex, norm_stack_num, norm_stack_cell
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
def build_config_all(config_proof: Cell) -> t.Dict[int, t.Any]:
|
|
@@ -109,3 +111,61 @@ def build_contract_state_info(
|
|
|
109
111
|
info.state = ContractState.NONEXIST
|
|
110
112
|
|
|
111
113
|
return info
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def decode_stack(items: t.List[t.Any]) -> StackItems:
|
|
117
|
+
"""
|
|
118
|
+
Decode VM stack items into internal Python structures.
|
|
119
|
+
|
|
120
|
+
Supports:
|
|
121
|
+
- int → int
|
|
122
|
+
- Cell/Slice → normalized cell
|
|
123
|
+
- Address → address cell
|
|
124
|
+
- VmTuple/list → recursive decode
|
|
125
|
+
- None → None
|
|
126
|
+
|
|
127
|
+
:param items: Raw VM stack items
|
|
128
|
+
:return: Normalized Python stack values
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
out: StackItems = []
|
|
132
|
+
for item in items:
|
|
133
|
+
if item is None:
|
|
134
|
+
out.append(None)
|
|
135
|
+
elif isinstance(item, int):
|
|
136
|
+
out.append(norm_stack_num(item))
|
|
137
|
+
elif isinstance(item, Address):
|
|
138
|
+
out.append(item.to_cell())
|
|
139
|
+
elif isinstance(item, (Cell, Slice)):
|
|
140
|
+
out.append(norm_stack_cell(item))
|
|
141
|
+
elif isinstance(item, VmTuple):
|
|
142
|
+
out.append(decode_stack(item.list))
|
|
143
|
+
elif isinstance(item, list):
|
|
144
|
+
out.append(decode_stack(item))
|
|
145
|
+
return out
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def encode_stack(items: t.List[StackItem]) -> t.List[t.Any]:
|
|
149
|
+
"""
|
|
150
|
+
Encode Python stack values into VM-compatible format.
|
|
151
|
+
|
|
152
|
+
Supports:
|
|
153
|
+
- int → int
|
|
154
|
+
- Cell/Slice → cell/slice
|
|
155
|
+
- Address → cell slice
|
|
156
|
+
- list/tuple → recursive encode
|
|
157
|
+
|
|
158
|
+
:param items: Normalized Python stack items
|
|
159
|
+
:return: VM-encoded stack values
|
|
160
|
+
"""
|
|
161
|
+
out: t.List[t.Any] = []
|
|
162
|
+
for item in items:
|
|
163
|
+
if isinstance(item, int):
|
|
164
|
+
out.append(item)
|
|
165
|
+
elif isinstance(item, Address):
|
|
166
|
+
out.append(item.to_cell().to_slice())
|
|
167
|
+
elif isinstance(item, (Cell, Slice)):
|
|
168
|
+
out.append(item)
|
|
169
|
+
elif isinstance(item, (list, tuple)):
|
|
170
|
+
out.append(encode_stack(list(item)))
|
|
171
|
+
return out
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
from .balancer import HttpBalancer
|
|
2
|
-
from .
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
from .clients import (
|
|
3
|
+
ChainstackHttpClient,
|
|
4
|
+
QuicknodeHttpClient,
|
|
5
|
+
TatumHttpClient,
|
|
6
|
+
TonapiHttpClient,
|
|
7
|
+
ToncenterHttpClient,
|
|
8
|
+
)
|
|
9
|
+
from .providers import (
|
|
10
|
+
TonapiHttpProvider,
|
|
11
|
+
ToncenterHttpProvider,
|
|
12
|
+
)
|
|
7
13
|
|
|
8
14
|
__all__ = [
|
|
9
15
|
"HttpBalancer",
|
|
10
16
|
"ChainstackHttpClient",
|
|
11
|
-
"ChainstackHttpProvider",
|
|
12
17
|
"QuicknodeHttpClient",
|
|
13
|
-
"QuicknodeHttpProvider",
|
|
14
18
|
"TatumHttpClient",
|
|
15
|
-
"TatumHttpProvider",
|
|
16
19
|
"TonapiHttpClient",
|
|
17
20
|
"TonapiHttpProvider",
|
|
18
21
|
"ToncenterHttpClient",
|
|
@@ -7,18 +7,19 @@ from contextlib import suppress
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from itertools import cycle
|
|
9
9
|
|
|
10
|
-
from pyapiq.exceptions import (
|
|
11
|
-
APIQException,
|
|
12
|
-
APIClientResponseError,
|
|
13
|
-
APIClientServerError,
|
|
14
|
-
APIClientTooManyRequestsError,
|
|
15
|
-
RateLimitExceeded,
|
|
16
|
-
)
|
|
17
10
|
from pytoniq_core import Transaction
|
|
18
11
|
|
|
19
12
|
from tonutils.clients.base import BaseClient
|
|
20
|
-
from tonutils.clients.http.quicknode import QuicknodeHttpClient
|
|
21
|
-
from tonutils.exceptions import
|
|
13
|
+
from tonutils.clients.http.clients.quicknode import QuicknodeHttpClient
|
|
14
|
+
from tonutils.exceptions import (
|
|
15
|
+
BalancerError,
|
|
16
|
+
ClientError,
|
|
17
|
+
TransportError,
|
|
18
|
+
ProviderError,
|
|
19
|
+
ProviderResponseError,
|
|
20
|
+
RunGetMethodError,
|
|
21
|
+
ProviderTimeoutError,
|
|
22
|
+
)
|
|
22
23
|
from tonutils.types import ClientType, ContractStateInfo, NetworkGlobalID
|
|
23
24
|
|
|
24
25
|
_T = t.TypeVar("_T")
|
|
@@ -52,6 +53,7 @@ class HttpBalancer(BaseClient):
|
|
|
52
53
|
*,
|
|
53
54
|
network: NetworkGlobalID = NetworkGlobalID.MAINNET,
|
|
54
55
|
clients: t.List[BaseClient],
|
|
56
|
+
request_timeout: float = 12.0,
|
|
55
57
|
) -> None:
|
|
56
58
|
"""
|
|
57
59
|
Initialize HTTP balancer.
|
|
@@ -71,6 +73,8 @@ class HttpBalancer(BaseClient):
|
|
|
71
73
|
|
|
72
74
|
:param network: Target TON network (mainnet or testnet)
|
|
73
75
|
:param clients: List of HTTP BaseClient instances to balance between
|
|
76
|
+
:param request_timeout: Maximum total time in seconds for a balancer operation,
|
|
77
|
+
including all failover attempts across providers
|
|
74
78
|
"""
|
|
75
79
|
self.network = network
|
|
76
80
|
|
|
@@ -83,6 +87,8 @@ class HttpBalancer(BaseClient):
|
|
|
83
87
|
self._retry_after_base = 1.0
|
|
84
88
|
self._retry_after_max = 10.0
|
|
85
89
|
|
|
90
|
+
self._request_timeout = request_timeout
|
|
91
|
+
|
|
86
92
|
def __init_clients(
|
|
87
93
|
self,
|
|
88
94
|
clients: t.List[BaseClient],
|
|
@@ -109,6 +115,25 @@ class HttpBalancer(BaseClient):
|
|
|
109
115
|
self._clients.append(client)
|
|
110
116
|
self._states.append(state)
|
|
111
117
|
|
|
118
|
+
@property
|
|
119
|
+
def provider(self) -> t.Any:
|
|
120
|
+
"""
|
|
121
|
+
Provider of the currently selected client.
|
|
122
|
+
|
|
123
|
+
:return: Provider instance of chosen HTTP client
|
|
124
|
+
"""
|
|
125
|
+
c = self._pick_client()
|
|
126
|
+
return c.provider
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def is_connected(self) -> bool:
|
|
130
|
+
"""
|
|
131
|
+
Check whether at least one underlying client is connected.
|
|
132
|
+
|
|
133
|
+
:return: True if any client is connected, otherwise False
|
|
134
|
+
"""
|
|
135
|
+
return any(c.is_connected for c in self._clients)
|
|
136
|
+
|
|
112
137
|
@property
|
|
113
138
|
def clients(self) -> t.Tuple[BaseClient, ...]:
|
|
114
139
|
"""
|
|
@@ -146,27 +171,6 @@ class HttpBalancer(BaseClient):
|
|
|
146
171
|
if state.retry_after is not None and state.retry_after > now
|
|
147
172
|
)
|
|
148
173
|
|
|
149
|
-
@property
|
|
150
|
-
def provider(self) -> t.Any:
|
|
151
|
-
"""
|
|
152
|
-
Provider of the currently selected client.
|
|
153
|
-
|
|
154
|
-
:return: Provider instance of chosen HTTP client
|
|
155
|
-
"""
|
|
156
|
-
if not self.is_connected:
|
|
157
|
-
raise ClientNotConnectedError(self)
|
|
158
|
-
client = self._pick_client()
|
|
159
|
-
return client.provider
|
|
160
|
-
|
|
161
|
-
@property
|
|
162
|
-
def is_connected(self) -> bool:
|
|
163
|
-
"""
|
|
164
|
-
Check whether at least one underlying client is connected.
|
|
165
|
-
|
|
166
|
-
:return: True if any client is connected, otherwise False
|
|
167
|
-
"""
|
|
168
|
-
return any(c.is_connected for c in self._clients)
|
|
169
|
-
|
|
170
174
|
async def __aenter__(self) -> HttpBalancer:
|
|
171
175
|
"""
|
|
172
176
|
Enter async context manager and connect all underlying clients.
|
|
@@ -219,7 +223,7 @@ class HttpBalancer(BaseClient):
|
|
|
219
223
|
height_candidates.append((wait, state.error_count, state))
|
|
220
224
|
|
|
221
225
|
if not height_candidates:
|
|
222
|
-
raise
|
|
226
|
+
raise BalancerError("no available HTTP clients")
|
|
223
227
|
|
|
224
228
|
height_candidates.sort(key=lambda x: (x[0], x[1]))
|
|
225
229
|
best_wait, best_err, _ = height_candidates[0]
|
|
@@ -292,39 +296,47 @@ class HttpBalancer(BaseClient):
|
|
|
292
296
|
:param func: Callable performing an operation using a client
|
|
293
297
|
:return: Result of the successful invocation
|
|
294
298
|
"""
|
|
295
|
-
last_exc: t.Optional[BaseException] = None
|
|
296
|
-
|
|
297
|
-
for _ in range(len(self._clients)):
|
|
298
|
-
if not self.alive_clients:
|
|
299
|
-
break
|
|
300
|
-
|
|
301
|
-
client = self._pick_client()
|
|
302
|
-
|
|
303
|
-
try:
|
|
304
|
-
result = await func(client)
|
|
305
|
-
except (APIClientTooManyRequestsError, RateLimitExceeded) as e:
|
|
306
|
-
self._mark_error(client, is_rate_limit=True)
|
|
307
|
-
last_exc = e
|
|
308
|
-
continue
|
|
309
|
-
except APIClientResponseError as e:
|
|
310
|
-
last_exc = e
|
|
311
|
-
break
|
|
312
|
-
except (APIClientServerError, APIQException) as e:
|
|
313
|
-
self._mark_error(client, is_rate_limit=False)
|
|
314
|
-
last_exc = e
|
|
315
|
-
continue
|
|
316
|
-
except Exception as e:
|
|
317
|
-
self._mark_error(client, is_rate_limit=False)
|
|
318
|
-
last_exc = e
|
|
319
|
-
continue
|
|
320
|
-
|
|
321
|
-
self._mark_success(client)
|
|
322
|
-
return result
|
|
323
|
-
|
|
324
|
-
if last_exc is not None:
|
|
325
|
-
raise last_exc
|
|
326
299
|
|
|
327
|
-
|
|
300
|
+
async def _run() -> _T:
|
|
301
|
+
last_exc: t.Optional[BaseException] = None
|
|
302
|
+
|
|
303
|
+
for _ in range(len(self._clients)):
|
|
304
|
+
if not self.alive_clients:
|
|
305
|
+
break
|
|
306
|
+
|
|
307
|
+
client = self._pick_client()
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
result = await func(client)
|
|
311
|
+
|
|
312
|
+
except RunGetMethodError:
|
|
313
|
+
raise
|
|
314
|
+
except ProviderResponseError as e:
|
|
315
|
+
is_rate_limit = e.code == 429
|
|
316
|
+
self._mark_error(client, is_rate_limit=is_rate_limit)
|
|
317
|
+
last_exc = e
|
|
318
|
+
continue
|
|
319
|
+
except (TransportError, ProviderError) as e:
|
|
320
|
+
self._mark_error(client, is_rate_limit=False)
|
|
321
|
+
last_exc = e
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
self._mark_success(client)
|
|
325
|
+
return result
|
|
326
|
+
|
|
327
|
+
if last_exc is not None:
|
|
328
|
+
raise last_exc
|
|
329
|
+
|
|
330
|
+
raise ClientError("all HTTP clients failed to process request.")
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
return await asyncio.wait_for(_run(), timeout=self._request_timeout)
|
|
334
|
+
except asyncio.TimeoutError as exc:
|
|
335
|
+
raise ProviderTimeoutError(
|
|
336
|
+
timeout=self._request_timeout,
|
|
337
|
+
endpoint="http balancer",
|
|
338
|
+
operation="failover request",
|
|
339
|
+
) from exc
|
|
328
340
|
|
|
329
341
|
async def _send_boc(self, boc: str) -> None:
|
|
330
342
|
async def _call(client: BaseClient) -> None:
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .chainstack import ChainstackHttpClient
|
|
2
|
+
from .quicknode import QuicknodeHttpClient
|
|
3
|
+
from .tatum import TatumHttpClient
|
|
4
|
+
from .tonapi import TonapiHttpClient
|
|
5
|
+
from .toncenter import ToncenterHttpClient
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ChainstackHttpClient",
|
|
9
|
+
"QuicknodeHttpClient",
|
|
10
|
+
"TatumHttpClient",
|
|
11
|
+
"TonapiHttpClient",
|
|
12
|
+
"ToncenterHttpClient",
|
|
13
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
from aiohttp import ClientSession
|
|
4
|
+
|
|
5
|
+
from tonutils.clients.http.clients.toncenter import ToncenterHttpClient
|
|
6
|
+
from tonutils.types import NetworkGlobalID, RetryPolicy
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ChainstackHttpClient(ToncenterHttpClient):
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
*,
|
|
14
|
+
network: NetworkGlobalID,
|
|
15
|
+
http_provider_url: str,
|
|
16
|
+
timeout: float = 10.0,
|
|
17
|
+
session: t.Optional[ClientSession] = None,
|
|
18
|
+
headers: t.Optional[t.Dict[str, str]] = None,
|
|
19
|
+
cookies: t.Optional[t.Dict[str, str]] = None,
|
|
20
|
+
rps_limit: t.Optional[int] = None,
|
|
21
|
+
rps_period: float = 1.0,
|
|
22
|
+
retry_policy: t.Optional[RetryPolicy] = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Initialize Chainstack HTTP client.
|
|
26
|
+
|
|
27
|
+
:param network: Target TON network (mainnet or testnet)
|
|
28
|
+
:param http_provider_url: Chainstack TON HTTP endpoint URL
|
|
29
|
+
You can obtain a personal endpoint on the Chainstack website: https://chainstack.com/
|
|
30
|
+
:param timeout: Total request timeout in seconds.
|
|
31
|
+
:param session: Optional external aiohttp session.
|
|
32
|
+
:param headers: Default headers for owned session.
|
|
33
|
+
:param cookies: Default cookies for owned session.
|
|
34
|
+
:param rps_limit: Optional requests-per-period limit.
|
|
35
|
+
:param rps_period: Rate limit period in seconds.
|
|
36
|
+
:param retry_policy: Optional retry policy that defines per-error-code retry rules
|
|
37
|
+
"""
|
|
38
|
+
super().__init__(
|
|
39
|
+
network=network,
|
|
40
|
+
base_url=http_provider_url,
|
|
41
|
+
timeout=timeout,
|
|
42
|
+
session=session,
|
|
43
|
+
headers=headers,
|
|
44
|
+
cookies=cookies,
|
|
45
|
+
rps_limit=rps_limit,
|
|
46
|
+
rps_period=rps_period,
|
|
47
|
+
retry_policy=retry_policy,
|
|
48
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
from aiohttp import ClientSession
|
|
4
|
+
|
|
5
|
+
from tonutils.clients.http.clients.toncenter import ToncenterHttpClient
|
|
6
|
+
from tonutils.types import NetworkGlobalID, RetryPolicy
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class QuicknodeHttpClient(ToncenterHttpClient):
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
*,
|
|
14
|
+
http_provider_url: str,
|
|
15
|
+
timeout: float = 10.0,
|
|
16
|
+
session: t.Optional[ClientSession] = None,
|
|
17
|
+
headers: t.Optional[t.Dict[str, str]] = None,
|
|
18
|
+
cookies: t.Optional[t.Dict[str, str]] = None,
|
|
19
|
+
rps_limit: t.Optional[int] = None,
|
|
20
|
+
rps_period: float = 1.0,
|
|
21
|
+
retry_policy: t.Optional[RetryPolicy] = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Initialize QuickNode HTTP client.
|
|
25
|
+
|
|
26
|
+
:param http_provider_url: QuickNode TON HTTP endpoint URL.
|
|
27
|
+
You can obtain a personal endpoint on the QuickNode website: https://www.quicknode.com/
|
|
28
|
+
:param timeout: Total request timeout in seconds.
|
|
29
|
+
:param session: Optional external aiohttp session.
|
|
30
|
+
:param headers: Default headers for owned session.
|
|
31
|
+
:param cookies: Default cookies for owned session.
|
|
32
|
+
:param rps_limit: Optional requests-per-period limit.
|
|
33
|
+
:param rps_period: Rate limit period in seconds.
|
|
34
|
+
:param retry_policy: Optional retry policy that defines per-error-code retry rules
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
super().__init__(
|
|
38
|
+
network=NetworkGlobalID.MAINNET,
|
|
39
|
+
base_url=http_provider_url,
|
|
40
|
+
timeout=timeout,
|
|
41
|
+
session=session,
|
|
42
|
+
headers=headers,
|
|
43
|
+
cookies=cookies,
|
|
44
|
+
rps_limit=rps_limit,
|
|
45
|
+
rps_period=rps_period,
|
|
46
|
+
retry_policy=retry_policy,
|
|
47
|
+
)
|