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.
Files changed (91) hide show
  1. tonutils/__init__.py +0 -2
  2. tonutils/__meta__.py +1 -1
  3. tonutils/cli.py +111 -0
  4. tonutils/clients/__init__.py +7 -11
  5. tonutils/clients/adnl/__init__.py +7 -3
  6. tonutils/clients/adnl/balancer.py +362 -168
  7. tonutils/clients/adnl/client.py +203 -67
  8. tonutils/clients/adnl/provider/config.py +24 -25
  9. tonutils/clients/adnl/provider/models.py +4 -0
  10. tonutils/clients/adnl/provider/provider.py +203 -160
  11. tonutils/clients/adnl/provider/transport.py +44 -33
  12. tonutils/clients/adnl/provider/workers/base.py +0 -2
  13. tonutils/clients/adnl/provider/workers/pinger.py +1 -1
  14. tonutils/clients/adnl/provider/workers/reader.py +3 -2
  15. tonutils/clients/adnl/{provider/builder.py → utils.py} +62 -2
  16. tonutils/clients/http/__init__.py +11 -8
  17. tonutils/clients/http/balancer.py +75 -63
  18. tonutils/clients/http/clients/__init__.py +13 -0
  19. tonutils/clients/http/clients/chainstack.py +48 -0
  20. tonutils/clients/http/clients/quicknode.py +47 -0
  21. tonutils/clients/http/clients/tatum.py +56 -0
  22. tonutils/clients/http/{tonapi/client.py → clients/tonapi.py} +31 -31
  23. tonutils/clients/http/{toncenter/client.py → clients/toncenter.py} +59 -48
  24. tonutils/clients/http/providers/__init__.py +4 -0
  25. tonutils/clients/http/providers/base.py +201 -0
  26. tonutils/clients/http/providers/response.py +85 -0
  27. tonutils/clients/http/providers/tonapi/__init__.py +3 -0
  28. tonutils/clients/http/{tonapi → providers/tonapi}/models.py +1 -0
  29. tonutils/clients/http/providers/tonapi/provider.py +125 -0
  30. tonutils/clients/http/providers/toncenter/__init__.py +3 -0
  31. tonutils/clients/http/{toncenter → providers/toncenter}/models.py +1 -0
  32. tonutils/clients/http/providers/toncenter/provider.py +119 -0
  33. tonutils/clients/http/utils.py +140 -0
  34. tonutils/clients/limiter.py +115 -0
  35. tonutils/contracts/__init__.py +4 -0
  36. tonutils/contracts/base.py +33 -20
  37. tonutils/contracts/dns/methods.py +2 -2
  38. tonutils/contracts/jetton/methods.py +2 -2
  39. tonutils/contracts/nft/methods.py +2 -2
  40. tonutils/contracts/nft/tlb.py +1 -1
  41. tonutils/{protocols/contract.py → contracts/protocol.py} +29 -29
  42. tonutils/contracts/telegram/methods.py +2 -2
  43. tonutils/contracts/vanity/vanity.py +1 -1
  44. tonutils/contracts/wallet/__init__.py +2 -0
  45. tonutils/contracts/wallet/base.py +3 -3
  46. tonutils/contracts/wallet/messages.py +1 -1
  47. tonutils/contracts/wallet/methods.py +2 -2
  48. tonutils/{protocols/wallet.py → contracts/wallet/protocol.py} +35 -35
  49. tonutils/contracts/wallet/versions/v5.py +3 -3
  50. tonutils/exceptions.py +146 -228
  51. tonutils/tonconnect/__init__.py +0 -0
  52. tonutils/tools/__init__.py +6 -0
  53. tonutils/tools/block_scanner/__init__.py +26 -0
  54. tonutils/tools/block_scanner/annotations.py +23 -0
  55. tonutils/tools/block_scanner/dispatcher.py +141 -0
  56. tonutils/tools/block_scanner/events.py +31 -0
  57. tonutils/tools/block_scanner/scanner.py +315 -0
  58. tonutils/tools/block_scanner/traversal.py +96 -0
  59. tonutils/tools/block_scanner/where.py +151 -0
  60. tonutils/tools/status_monitor/__init__.py +3 -0
  61. tonutils/tools/status_monitor/console.py +157 -0
  62. tonutils/tools/status_monitor/models.py +27 -0
  63. tonutils/tools/status_monitor/monitor.py +295 -0
  64. tonutils/types.py +125 -2
  65. tonutils/utils.py +3 -3
  66. {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b4.dist-info}/METADATA +2 -5
  67. tonutils-2.0.1b4.dist-info/RECORD +108 -0
  68. tonutils-2.0.1b4.dist-info/entry_points.txt +2 -0
  69. tonutils/clients/adnl/provider/limiter.py +0 -56
  70. tonutils/clients/adnl/stack.py +0 -64
  71. tonutils/clients/http/chainstack/__init__.py +0 -4
  72. tonutils/clients/http/chainstack/client.py +0 -63
  73. tonutils/clients/http/chainstack/provider.py +0 -44
  74. tonutils/clients/http/quicknode/__init__.py +0 -4
  75. tonutils/clients/http/quicknode/client.py +0 -60
  76. tonutils/clients/http/quicknode/provider.py +0 -42
  77. tonutils/clients/http/tatum/__init__.py +0 -4
  78. tonutils/clients/http/tatum/client.py +0 -66
  79. tonutils/clients/http/tatum/provider.py +0 -53
  80. tonutils/clients/http/tonapi/__init__.py +0 -4
  81. tonutils/clients/http/tonapi/provider.py +0 -150
  82. tonutils/clients/http/tonapi/stack.py +0 -71
  83. tonutils/clients/http/toncenter/__init__.py +0 -4
  84. tonutils/clients/http/toncenter/provider.py +0 -145
  85. tonutils/clients/http/toncenter/stack.py +0 -73
  86. tonutils/protocols/__init__.py +0 -9
  87. tonutils-2.0.1b2.dist-info/RECORD +0 -98
  88. /tonutils/{protocols/client.py → clients/protocol.py} +0 -0
  89. {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b4.dist-info}/WHEEL +0 -0
  90. {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b4.dist-info}/licenses/LICENSE +0 -0
  91. {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, timeout: int) -> None:
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 timeout: Timeout in seconds for connection and I/O operations
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.timeout = timeout
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 AdnlTransportStateError("`writer` is not initialized")
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 AdnlTransportCipherError("`encryption`")
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 AdnlTransportCipherError("`decryption`")
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 AdnlTransportStateError("already connected")
167
+ raise self._error("connect", "already connected")
165
168
 
166
169
  self.loop = asyncio.get_running_loop()
167
170
 
168
- self.reader, self.writer = await asyncio.wait_for(
169
- asyncio.open_connection(self.server.host, self.server.port),
170
- timeout=self.timeout,
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 AdnlTransportError("failed to initialize TCP streams")
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.timeout,
194
+ timeout=self.connect_timeout,
184
195
  )
185
196
  except asyncio.IncompleteReadError as exc:
186
- raise AdnlHandshakeError(
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 AdnlHandshakeError(
191
- f"Timed out waiting for initial ADNL handshake ({self.timeout}s)"
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 AdnlTransportStateError("transport is not connected")
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 AdnlTransportStateError("transport is not connected")
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 AdnlTransportStateError("`reader` is not initialized")
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 AdnlTransportFrameError(f"non-positive length `{data_len}`")
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 AdnlTransportFrameError("frame is too short")
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 AdnlTransportFrameError("checksum mismatch")
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,
@@ -63,8 +63,6 @@ class BaseWorker(ABC):
63
63
  await self._run()
64
64
  except asyncio.CancelledError:
65
65
  pass
66
- except (Exception,):
67
- pass
68
66
  finally:
69
67
  self._running = False
70
68
 
@@ -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.timeout)
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 AdnlServerError
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 = AdnlServerError(
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 .chainstack import ChainstackHttpClient, ChainstackHttpProvider
3
- from .quicknode import QuicknodeHttpClient, QuicknodeHttpProvider
4
- from .tatum import TatumHttpClient, TatumHttpProvider
5
- from .tonapi import TonapiHttpClient, TonapiHttpProvider
6
- from .toncenter import ToncenterHttpClient, ToncenterHttpProvider
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 ClientError, ClientNotConnectedError
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 ClientError("No available HTTP clients in HttpBalancer.")
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
- raise ClientError("All HTTP clients failed to process request.")
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
+ )