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
@@ -7,42 +7,54 @@ from contextlib import suppress
7
7
  from dataclasses import dataclass
8
8
  from itertools import cycle
9
9
 
10
- from pytoniq_core import Address, Transaction
10
+ from pytoniq_core import Address, BlockIdExt, Block, Transaction
11
11
 
12
- from tonutils.clients.adnl.client import AdnlClient
12
+ from tonutils.clients.adnl.client import LiteClient
13
13
  from tonutils.clients.adnl.provider import AdnlProvider
14
- from tonutils.clients.adnl.provider.config import TONClient
15
- from tonutils.clients.adnl.provider.limiter import PriorityLimiter
16
- from tonutils.clients.adnl.provider.models import GlobalConfig
17
- from tonutils.clients.adnl.stack import decode_stack, encode_stack
14
+ from tonutils.clients.adnl.provider.config import (
15
+ get_mainnet_global_config,
16
+ get_testnet_global_config,
17
+ )
18
+ from tonutils.clients.adnl.provider.models import GlobalConfig, MasterchainInfo
19
+ from tonutils.clients.adnl.utils import decode_stack, encode_stack
18
20
  from tonutils.clients.base import BaseClient
21
+ from tonutils.clients.limiter import RateLimiter
19
22
  from tonutils.exceptions import (
20
- AdnlBalancerConnectionError,
21
- ClientNotConnectedError,
22
- RateLimitExceededError,
23
23
  ClientError,
24
+ BalancerError,
25
+ RunGetMethodError,
26
+ ProviderResponseError,
27
+ TransportError,
28
+ ProviderError,
29
+ ProviderTimeoutError,
30
+ )
31
+ from tonutils.types import (
32
+ ClientType,
33
+ ContractStateInfo,
34
+ NetworkGlobalID,
35
+ RetryPolicy,
36
+ WorkchainID,
24
37
  )
25
- from tonutils.types import ClientType, ContractStateInfo, NetworkGlobalID
26
38
 
27
39
  _T = t.TypeVar("_T")
28
40
 
29
41
 
30
42
  @dataclass
31
- class AdnlClientState:
43
+ class LiteClientState:
32
44
  """
33
- Internal state container for an ADNL client.
45
+ Internal state container for a lite-server client.
34
46
 
35
47
  Tracks error count and cooldown timeout for retry scheduling.
36
48
  """
37
49
 
38
- client: AdnlClient
50
+ client: LiteClient
39
51
  retry_after: t.Optional[float] = None
40
52
  error_count: int = 0
41
53
 
42
54
 
43
- class AdnlBalancer(BaseClient):
55
+ class LiteBalancer(BaseClient):
44
56
  """
45
- Multi-provider ADNL client with automatic failover and load balancing.
57
+ Multi-client lite-server balancer with automatic failover and load balancing.
46
58
 
47
59
  Selects the best available lite-server using height, ping metrics and
48
60
  round-robin tie-breaking.
@@ -54,13 +66,14 @@ class AdnlBalancer(BaseClient):
54
66
  self,
55
67
  *,
56
68
  network: NetworkGlobalID = NetworkGlobalID.MAINNET,
57
- clients: t.List[AdnlClient],
58
- connect_timeout: int = 2,
69
+ clients: t.List[LiteClient],
70
+ connect_timeout: float = 2.0,
71
+ request_timeout: float = 12.0,
59
72
  ) -> None:
60
73
  """
61
- Initialize ADNL balancer.
74
+ Initialize lite-server balancer.
62
75
 
63
- It is recommended to build underlying AdnlClient instances from
76
+ It is recommended to build underlying LiteClient instances from
64
77
  private lite-server configurations for better stability and performance.
65
78
  You can obtain private lite-server configs from:
66
79
  - Tonconsole website: https://tonconsole.com/
@@ -69,17 +82,21 @@ class AdnlBalancer(BaseClient):
69
82
  Public free lite-server data may also be used via `from_network_config()`.
70
83
 
71
84
  :param network: Target TON network (mainnet or testnet)
72
- :param clients: List of AdnlClient instances to balance between
73
- :param connect_timeout: Timeout in seconds for connect/reconnect
85
+ :param clients: List of LiteClient instances to balance between
86
+ :param connect_timeout: Timeout in seconds for connect/reconnect attempts
87
+ :param request_timeout: Maximum total time in seconds for a balancer operation,
88
+ including all failover attempts across lite-servers
74
89
  """
75
90
  self.network: NetworkGlobalID = network
76
91
 
77
- self._clients: t.List[AdnlClient] = []
78
- self._states: t.List[AdnlClientState] = []
92
+ self._clients: t.List[LiteClient] = []
93
+ self._states: t.List[LiteClientState] = []
79
94
  self.__init_clients(clients)
80
95
 
81
96
  self._rr = cycle(self._clients)
97
+
82
98
  self._connect_timeout = connect_timeout
99
+ self._request_timeout = request_timeout
83
100
 
84
101
  self._health_interval = 5.5
85
102
  self._health_task: t.Optional[asyncio.Task] = None
@@ -89,41 +106,60 @@ class AdnlBalancer(BaseClient):
89
106
 
90
107
  def __init_clients(
91
108
  self,
92
- clients: t.List[AdnlClient],
109
+ clients: t.List[LiteClient],
93
110
  ) -> None:
94
111
  """
95
- Validate and register input ADNL clients.
112
+ Validate and register input lite-server clients.
96
113
 
97
114
  Ensures correct client type and network assignment.
98
115
  """
99
116
  for client in clients:
100
117
  if client.TYPE != ClientType.ADNL:
101
118
  raise ClientError(
102
- "AdnlBalancer can work only with ADNL clients, "
119
+ "LiteBalancer can work only with LiteClient instances, "
103
120
  f"got {client.__class__.__name__}."
104
121
  )
105
122
 
106
123
  client.network = self.network
107
124
 
108
- state = AdnlClientState(client=client)
125
+ state = LiteClientState(client=client)
109
126
  self._clients.append(client)
110
127
  self._states.append(state)
111
128
 
112
129
  @property
113
- def clients(self) -> t.Tuple[AdnlClient, ...]:
130
+ def provider(self) -> AdnlProvider:
131
+ """
132
+ Provider of the currently selected lite-server client.
133
+
134
+ :return: AdnlProvider instance of chosen client
135
+ """
136
+ c = self._pick_client()
137
+ return c.provider
138
+
139
+ @property
140
+ def is_connected(self) -> bool:
141
+ """
142
+ Check whether at least one underlying lite-server client is connected.
143
+
144
+ :return: True if any client is connected, otherwise False
145
+ """
146
+ return any(c.is_connected for c in self._clients)
147
+
148
+ @property
149
+ def clients(self) -> t.Tuple[LiteClient, ...]:
114
150
  """
115
- List of all registered ADNL clients.
151
+ List of all registered lite-server clients.
116
152
 
117
- :return: Tuple of AdnlClient objects
153
+ :return: Tuple of LiteClient objects
118
154
  """
119
155
  return tuple(self._clients)
120
156
 
121
157
  @property
122
- def alive_clients(self) -> t.Tuple[AdnlClient, ...]:
158
+ def alive_clients(self) -> t.Tuple[LiteClient, ...]:
123
159
  """
124
- ADNL clients that are allowed to send requests now.
160
+ Lite-server clients that are allowed to send requests now.
125
161
 
126
- :return: Tuple of available AdnlClient instances
162
+ :return: Tuple of available LiteClient instances
127
163
  """
128
164
  now = time.monotonic()
129
165
  return tuple(
@@ -134,11 +170,11 @@ class AdnlBalancer(BaseClient):
134
170
  )
135
171
 
136
172
  @property
137
- def dead_clients(self) -> t.Tuple[AdnlClient, ...]:
173
+ def dead_clients(self) -> t.Tuple[LiteClient, ...]:
138
174
  """
139
- ADNL clients currently in cooldown or disconnected.
175
+ Lite-server clients currently in cooldown or disconnected.
140
176
 
141
- :return: Tuple of unavailable AdnlClient instances
177
+ :return: Tuple of unavailable LiteClient instances
142
178
  """
143
179
  now = time.monotonic()
144
180
  return tuple(
@@ -148,29 +184,7 @@ class AdnlBalancer(BaseClient):
148
184
  or (state.retry_after is not None and state.retry_after > now)
149
185
  )
150
186
 
151
- @property
152
- def provider(self) -> AdnlProvider:
153
- """
154
- Provider of the currently selected ADNL client.
155
-
156
- :return: AdnlProvider instance of chosen client
157
- :raises ClientNotConnectedError: If no clients are connected
158
- """
159
- if not self.is_connected:
160
- raise ClientNotConnectedError(self)
161
- c = self._pick_client()
162
- return c.provider
163
-
164
- @property
165
- def is_connected(self) -> bool:
166
- """
167
- Check whether at least one underlying ADNL client is connected.
168
-
169
- :return: True if any client is connected, otherwise False
170
- """
171
- return any(c.is_connected for c in self._clients)
172
-
173
- async def __aenter__(self) -> AdnlBalancer:
187
+ async def __aenter__(self) -> LiteBalancer:
174
188
  """
175
189
  Enter async context manager and connect underlying clients.
176
190
 
@@ -195,15 +209,17 @@ class AdnlBalancer(BaseClient):
195
209
  *,
196
210
  network: NetworkGlobalID = NetworkGlobalID.MAINNET,
197
211
  config: t.Union[GlobalConfig, t.Dict[str, t.Any]],
198
- timeout: int = 10,
199
- connect_timeout: int = 2,
212
+ connect_timeout: float = 2.0,
213
+ request_timeout: float = 12.0,
214
+ client_connect_timeout: float = 1.5,
215
+ client_request_timeout: float = 5.0,
200
216
  rps_limit: t.Optional[int] = None,
201
217
  rps_period: float = 1.0,
202
- rps_retries: int = 2,
203
- rps_per_provider: bool = False,
204
- ) -> AdnlBalancer:
218
+ rps_per_client: bool = False,
219
+ retry_policy: t.Optional[RetryPolicy] = None,
220
+ ) -> LiteBalancer:
205
221
  """
206
- Build ADNL balancer from a lite-server config.
222
+ Build lite-server balancer from a configuration.
207
223
 
208
224
  For best performance, it is recommended to use a private lite-server
209
225
  configuration. You can obtain private configs from:
@@ -214,67 +230,74 @@ class AdnlBalancer(BaseClient):
214
230
 
215
231
  :param network: Target TON network
216
232
  :param config: GlobalConfig instance or raw dict
217
- :param timeout: Lite-server request timeout in seconds
218
- :param connect_timeout: Timeout in seconds for connect/reconnect attempts
219
- :param rps_limit: Optional shared requests-per-second limit
233
+ :param connect_timeout: Timeout in seconds for a single connect/reconnect attempt
234
+ performed by the balancer during failover.
235
+ :param request_timeout: Maximum total time in seconds for a single balancer operation,
236
+ including all failover attempts across clients.
237
+ :param client_connect_timeout: Timeout in seconds for connect/handshake performed by an
238
+ individual lite-server client.
239
+ :param client_request_timeout: Timeout in seconds for a single request executed by an
240
+ individual lite-server client.
241
+ :param rps_limit: Optional requests-per-second limit
220
242
  :param rps_period: Time window in seconds for RPS limit
221
- :param rps_retries: Number of retries on rate limiting
222
- :param rps_per_provider: Whether to create per-provider limiters
223
- :return: Configured AdnlBalancer instance
243
+ :param rps_per_client: Whether to create per-client limiters
244
+ :param retry_policy: Optional retry policy that defines per-error-code retry rules
245
+ :return: Configured LiteBalancer instance
224
246
  """
225
247
  if isinstance(config, dict):
226
248
  config = GlobalConfig(**config)
227
249
 
228
- shared_limiter: t.Optional[PriorityLimiter] = None
229
- if rps_limit is not None and not rps_per_provider:
230
- shared_limiter = PriorityLimiter(rps_limit, rps_period)
231
-
232
- clients: t.List[AdnlClient] = []
233
- for node in config.liteservers:
234
- if rps_per_provider:
235
- _limiter = (
236
- PriorityLimiter(rps_limit, rps_period)
237
- if rps_limit is not None
238
- else None
250
+ shared_limiter: t.Optional[RateLimiter] = None
251
+ if rps_limit is not None and not rps_per_client:
252
+ shared_limiter = RateLimiter(rps_limit, rps_period)
253
+
254
+ clients: t.List[LiteClient] = []
255
+ for ls in config.liteservers:
256
+ limiter = (
257
+ RateLimiter(rps_limit, rps_period)
258
+ if rps_per_client and rps_limit is not None
259
+ else shared_limiter
260
+ )
261
+ client_rps_limit = rps_limit if rps_per_client else None
262
+
263
+ clients.append(
264
+ LiteClient(
265
+ network=network,
266
+ ip=ls.host,
267
+ port=ls.port,
268
+ public_key=ls.id,
269
+ connect_timeout=client_connect_timeout,
270
+ request_timeout=client_request_timeout,
271
+ rps_limit=client_rps_limit,
272
+ rps_period=rps_period,
273
+ limiter=limiter,
274
+ retry_policy=retry_policy,
239
275
  )
240
- _rps_limit = rps_limit
241
- else:
242
- _limiter = shared_limiter
243
- _rps_limit = None
244
-
245
- client = AdnlClient(
246
- network=network,
247
- ip=node.host,
248
- port=node.port,
249
- public_key=node.id,
250
- timeout=timeout,
251
- rps_limit=_rps_limit,
252
- rps_retries=rps_retries,
253
- rps_period=rps_period,
254
- limiter=_limiter,
255
276
  )
256
- clients.append(client)
257
277
 
258
278
  return cls(
259
279
  network=network,
260
280
  clients=clients,
261
281
  connect_timeout=connect_timeout,
282
+ request_timeout=request_timeout,
262
283
  )
263
284
 
264
285
  @classmethod
265
- async def from_network_config(
286
+ def from_network_config(
266
287
  cls,
267
288
  *,
268
289
  network: NetworkGlobalID = NetworkGlobalID.MAINNET,
269
- timeout: int = 10,
270
- connect_timeout: int = 2,
290
+ connect_timeout: float = 2.0,
291
+ request_timeout: float = 12.0,
292
+ client_connect_timeout: float = 1.5,
293
+ client_request_timeout: float = 5.0,
271
294
  rps_limit: t.Optional[int] = None,
272
295
  rps_period: float = 1.0,
273
- rps_retries: int = 2,
274
- rps_per_provider: bool = False,
275
- ) -> AdnlBalancer:
296
+ rps_per_client: bool = False,
297
+ retry_policy: t.Optional[RetryPolicy] = None,
298
+ ) -> LiteBalancer:
276
299
  """
277
- Build ADNL balancer using global config fetched from ton.org.
300
+ Build lite-server balancer using global config fetched from ton.org.
278
301
 
279
302
  Public lite-servers available in the global network configuration are
280
303
  free to use but may be unstable under load. For higher reliability and
@@ -284,56 +307,62 @@ class AdnlBalancer(BaseClient):
284
307
  - dTON telegram bot: https://t.me/dtontech_bot (https://dton.io/)
285
308
 
286
309
  :param network: Target TON network
287
- :param timeout: Lite-server request timeout in seconds
288
- :param connect_timeout: Timeout in seconds for connect/reconnect attempts
289
- :param rps_limit: Optional shared requests-per-second limit
310
+ :param connect_timeout: Timeout in seconds for a single connect/reconnect attempt
311
+ performed by the balancer during failover.
312
+ :param request_timeout: Maximum total time in seconds for a single balancer operation,
313
+ including all failover attempts across clients.
314
+ :param client_connect_timeout: Timeout in seconds for connect/handshake performed by an
315
+ individual lite-server client.
316
+ :param client_request_timeout: Timeout in seconds for a single request executed by an
317
+ individual lite-server client.
318
+ :param rps_limit: Optional requests-per-second limit
290
319
  :param rps_period: Time window in seconds for RPS limit
291
- :param rps_retries: Number of retries on rate limiting
292
- :param rps_per_provider: Whether to create per-provider limiters
293
- :return: Configured AdnlBalancer instance
320
+ :param rps_per_client: Whether to create per-client limiters
321
+ :param retry_policy: Optional retry policy that defines per-error-code retry rules
322
+ :return: Configured LiteBalancer instance
294
323
  """
295
- ton_client = TONClient()
296
324
  config_getters = {
297
- NetworkGlobalID.MAINNET: ton_client.mainnet_global_config,
298
- NetworkGlobalID.TESTNET: ton_client.testnet_global_config,
325
+ NetworkGlobalID.MAINNET: get_mainnet_global_config,
326
+ NetworkGlobalID.TESTNET: get_testnet_global_config,
299
327
  }
300
- async with ton_client:
301
- config = await config_getters[network]()
328
+ config = config_getters[network]()
302
329
  return cls.from_config(
303
330
  network=network,
304
331
  config=config,
305
- timeout=timeout,
332
+ connect_timeout=connect_timeout,
333
+ request_timeout=request_timeout,
334
+ client_connect_timeout=client_connect_timeout,
335
+ client_request_timeout=client_request_timeout,
306
336
  rps_limit=rps_limit,
307
337
  rps_period=rps_period,
308
- rps_retries=rps_retries,
309
- rps_per_provider=rps_per_provider,
310
- connect_timeout=connect_timeout,
338
+ rps_per_client=rps_per_client,
339
+ retry_policy=retry_policy,
311
340
  )
312
341
 
313
- def _pick_client(self) -> AdnlClient:
342
+ def _pick_client(self) -> LiteClient:
314
343
  """
315
- Select the best available ADNL client.
344
+ Select the best available lite-server client.
316
345
 
317
346
  Selection criteria:
318
347
  - highest known masterchain seqno
319
348
  - minimal ping RTT and age among same-height clients
320
349
  - round-robin fallback if no height information
321
350
  """
322
- alive_clients = list(self.alive_clients)
351
+ alive = list(self.alive_clients)
323
352
 
324
- if not alive_clients:
325
- raise AdnlBalancerConnectionError("No alive lite-server clients available.")
353
+ if not alive:
354
+ raise BalancerError("no alive lite-server clients available")
326
355
 
327
356
  height_candidates: t.List[
328
357
  t.Tuple[
329
358
  int,
330
359
  t.Optional[float],
331
360
  t.Optional[float],
332
- AdnlClient,
361
+ LiteClient,
333
362
  ]
334
363
  ] = []
335
364
 
336
- for client in alive_clients:
365
+ for client in alive:
337
366
  mc_block = client.provider.last_mc_block
338
367
  if mc_block is None:
339
368
  continue
@@ -357,12 +386,12 @@ class AdnlBalancer(BaseClient):
357
386
 
358
387
  for _ in range(len(self._clients)):
359
388
  candidate = next(self._rr)
360
- if candidate in alive_clients and candidate.is_connected:
389
+ if candidate in alive and candidate.is_connected:
361
390
  return candidate
362
391
 
363
- return alive_clients[0]
392
+ return alive[0]
364
393
 
365
- def _mark_success(self, client: AdnlClient) -> None:
394
+ def _mark_success(self, client: LiteClient) -> None:
366
395
  """
367
396
  Reset error state for a successful client.
368
397
 
@@ -374,7 +403,7 @@ class AdnlBalancer(BaseClient):
374
403
  state.retry_after = None
375
404
  break
376
405
 
377
- def _mark_error(self, client: AdnlClient, is_rate_limit: bool) -> None:
406
+ def _mark_error(self, client: LiteClient, is_rate_limit: bool) -> None:
378
407
  """
379
408
  Update error state and schedule retry cooldown.
380
409
 
@@ -408,50 +437,63 @@ class AdnlBalancer(BaseClient):
408
437
  Execute a provider operation with automatic failover.
409
438
 
410
439
  Iterates through available lite-servers until one succeeds
411
- or all providers fail.
440
+ or all fail.
412
441
 
413
442
  :param func: Callable performing an operation using an AdnlProvider
414
443
  :return: Result of the successful invocation
415
444
  """
416
- last_exc: t.Optional[BaseException] = None
417
445
 
418
- for _ in range(len(self._clients)):
419
- if not self.alive_clients:
420
- break
446
+ async def _run() -> _T:
447
+ last_exc: t.Optional[BaseException] = None
448
+
449
+ for _ in range(len(self._clients)):
450
+ if not self.alive_clients:
451
+ break
452
+
453
+ client = self._pick_client()
421
454
 
422
- client = self._pick_client()
455
+ if not client.provider.is_connected:
456
+ try:
457
+ await asyncio.wait_for(
458
+ client.provider.reconnect(),
459
+ timeout=self._connect_timeout,
460
+ )
461
+ except Exception as e:
462
+ self._mark_error(client, is_rate_limit=False)
463
+ last_exc = e
464
+ continue
423
465
 
424
- if not client.provider.is_connected:
425
466
  try:
426
- await asyncio.wait_for(
427
- client.provider.reconnect(),
428
- timeout=self._connect_timeout,
429
- )
430
- except Exception as e:
467
+ result = await func(client.provider)
468
+
469
+ except RunGetMethodError:
470
+ raise
471
+ except ProviderResponseError as e:
472
+ is_rate_limit = e.code in {228, 5556}
473
+ self._mark_error(client, is_rate_limit=is_rate_limit)
474
+ last_exc = e
475
+ continue
476
+ except (TransportError, ProviderError) as e:
431
477
  self._mark_error(client, is_rate_limit=False)
432
478
  last_exc = e
433
479
  continue
434
480
 
435
- try:
436
- result = await func(client.provider)
437
- except RateLimitExceededError as e:
438
- self._mark_error(client, is_rate_limit=True)
439
- last_exc = e
440
- continue
441
- except Exception as e:
442
- self._mark_error(client, is_rate_limit=False)
443
- last_exc = e
444
- continue
481
+ self._mark_success(client)
482
+ return result
445
483
 
446
- self._mark_success(client)
447
- return result
484
+ if last_exc is not None:
485
+ raise last_exc
448
486
 
449
- if last_exc is not None:
450
- raise last_exc
487
+ raise BalancerError("all lite-servers failed to process request")
451
488
 
452
- raise AdnlBalancerConnectionError(
453
- "All lite-server providers failed to process request"
454
- )
489
+ try:
490
+ return await asyncio.wait_for(_run(), timeout=self._request_timeout)
491
+ except asyncio.TimeoutError as exc:
492
+ raise ProviderTimeoutError(
493
+ timeout=self._request_timeout,
494
+ endpoint="lite balancer",
495
+ operation="failover request",
496
+ ) from exc
455
497
 
456
498
  async def _send_boc(self, boc: str) -> None:
457
499
  async def _call(provider: AdnlProvider) -> None:
@@ -486,12 +528,12 @@ class AdnlBalancer(BaseClient):
486
528
 
487
529
  curr_lt = state.last_transaction_lt
488
530
  curr_hash = state.last_transaction_hash
489
- transactions: list[Transaction] = []
531
+ transactions: t.List[Transaction] = []
490
532
 
491
533
  while len(transactions) < limit and curr_lt != 0:
492
534
  batch_size = min(16, limit - len(transactions))
493
535
 
494
- async def _call(provider: AdnlProvider) -> list[Transaction]:
536
+ async def _call(provider: AdnlProvider) -> t.List[Transaction]:
495
537
  return await provider.get_transactions(
496
538
  account=account,
497
539
  count=batch_size,
@@ -504,7 +546,7 @@ class AdnlBalancer(BaseClient):
504
546
  break
505
547
 
506
548
  if to_lt > 0 and txs[-1].lt <= to_lt:
507
- trimmed: list[Transaction] = []
549
+ trimmed: t.List[Transaction] = []
508
550
  for tx in txs:
509
551
  if tx.lt <= to_lt:
510
552
  break
@@ -541,6 +583,12 @@ class AdnlBalancer(BaseClient):
541
583
  return await self._with_failover(_call)
542
584
 
543
585
  def _ensure_health_task(self) -> None:
586
+ """
587
+ Ensure background health check task is running.
588
+
589
+ Starts a periodic reconnect loop for unavailable clients
590
+ if it is not already active.
591
+ """
544
592
  if self._health_task is not None and not self._health_task.done():
545
593
  return
546
594
 
@@ -551,8 +599,13 @@ class AdnlBalancer(BaseClient):
551
599
  )
552
600
 
553
601
  async def _health_loop(self) -> None:
602
+ """
603
+ Periodically attempt to reconnect dead lite-server clients.
554
604
 
555
- async def _recon(c: AdnlClient) -> None:
605
+ Runs until cancelled.
606
+ """
607
+
608
+ async def _recon(c: LiteClient) -> None:
556
609
  with suppress(Exception):
557
610
  await asyncio.wait_for(
558
611
  c.reconnect(),
@@ -576,7 +629,7 @@ class AdnlBalancer(BaseClient):
576
629
  self._ensure_health_task()
577
630
  return
578
631
 
579
- async def _con(client: AdnlClient) -> None:
632
+ async def _con(client: LiteClient) -> None:
580
633
  with suppress(asyncio.TimeoutError):
581
634
  await asyncio.wait_for(
582
635
  client.connect(),
@@ -590,9 +643,7 @@ class AdnlBalancer(BaseClient):
590
643
  self._ensure_health_task()
591
644
  return
592
645
 
593
- raise AdnlBalancerConnectionError(
594
- "All lite-servers failed to establish connection."
595
- )
646
+ raise BalancerError("all lite-servers failed to establish connection")
596
647
 
597
648
  async def close(self) -> None:
598
649
  task, self._health_task = self._health_task, None
@@ -604,3 +655,146 @@ class AdnlBalancer(BaseClient):
604
655
 
605
656
  tasks = [client.close() for client in self._clients]
606
657
  await asyncio.gather(*tasks, return_exceptions=True)
658
+
659
+ async def get_time(self) -> int:
660
+ """
661
+ Fetch current network time from lite-server.
662
+
663
+ :return: Current UNIX timestamp
664
+ """
665
+
666
+ async def _call(provider: AdnlProvider) -> int:
667
+ return await provider.get_time()
668
+
669
+ return await self._with_failover(_call)
670
+
671
+ async def get_version(self) -> int:
672
+ """
673
+ Fetch lite-server protocol version.
674
+
675
+ :return: Version number
676
+ """
677
+
678
+ async def _call(provider: AdnlProvider) -> int:
679
+ return await provider.get_version()
680
+
681
+ return await self._with_failover(_call)
682
+
683
+ async def wait_masterchain_seqno(
684
+ self,
685
+ seqno: int,
686
+ timeout_ms: int,
687
+ schema_name: str,
688
+ data: t.Optional[dict] = None,
689
+ ) -> dict:
690
+ """
691
+ Combine waitMasterchainSeqno with another lite-server query.
692
+
693
+ :param seqno: Masterchain seqno to wait for
694
+ :param timeout_ms: Wait timeout in milliseconds
695
+ :param schema_name: Lite-server TL method name without prefix
696
+ :param data: Additional method arguments
697
+ :return: Lite-server response as dictionary
698
+ """
699
+
700
+ async def _call(provider: AdnlProvider) -> dict:
701
+ return await provider.wait_masterchain_seqno(
702
+ seqno=seqno,
703
+ timeout_ms=timeout_ms,
704
+ schema_name=schema_name,
705
+ data=data,
706
+ )
707
+
708
+ return await self._with_failover(_call)
709
+
710
+ async def get_masterchain_info(self) -> MasterchainInfo:
711
+ """
712
+ Fetch basic masterchain information.
713
+
714
+ :return: MasterchainInfo instance
715
+ """
716
+
717
+ async def _call(provider: AdnlProvider) -> MasterchainInfo:
718
+ return await provider.get_masterchain_info()
719
+
720
+ return await self._with_failover(_call)
721
+
722
+ async def lookup_block(
723
+ self,
724
+ workchain: WorkchainID,
725
+ shard: int,
726
+ seqno: t.Optional[int] = None,
727
+ lt: t.Optional[int] = None,
728
+ utime: t.Optional[int] = None,
729
+ ) -> t.Tuple[BlockIdExt, Block]:
730
+ """
731
+ Locate a block by workchain/shard and one of seqno/lt/utime.
732
+
733
+ :param workchain: Workchain identifier
734
+ :param shard: Shard identifier
735
+ :param seqno: Block sequence number
736
+ :param lt: Logical time filter
737
+ :param utime: UNIX time filter
738
+ :return: Tuple of BlockIdExt and deserialized Block
739
+ """
740
+
741
+ async def _call(provider: AdnlProvider) -> t.Tuple[BlockIdExt, Block]:
742
+ return await provider.lookup_block(
743
+ workchain=workchain,
744
+ shard=shard,
745
+ seqno=seqno,
746
+ lt=lt,
747
+ utime=utime,
748
+ )
749
+
750
+ return await self._with_failover(_call)
751
+
752
+ async def get_block_header(
753
+ self,
754
+ block: BlockIdExt,
755
+ ) -> t.Tuple[BlockIdExt, Block]:
756
+ """
757
+ Fetch and deserialize block header by BlockIdExt.
758
+
759
+ :param block: BlockIdExt to query
760
+ :return: Tuple of BlockIdExt and deserialized Block
761
+ """
762
+
763
+ async def _call(provider: AdnlProvider) -> t.Tuple[BlockIdExt, Block]:
764
+ return await provider.get_block_header(block)
765
+
766
+ return await self._with_failover(_call)
767
+
768
+ async def get_block_transactions_ext(
769
+ self,
770
+ block: BlockIdExt,
771
+ count: int = 1024,
772
+ ) -> t.List[Transaction]:
773
+ """
774
+ Fetch extended block transactions list.
775
+
776
+ :param block: Target block identifier
777
+ :param count: Maximum number of transactions per request
778
+ :return: List of deserialized Transaction objects
779
+ """
780
+
781
+ async def _call(provider: AdnlProvider) -> t.List[Transaction]:
782
+ return await provider.get_block_transactions_ext(block, count=count)
783
+
784
+ return await self._with_failover(_call)
785
+
786
+ async def get_all_shards_info(
787
+ self,
788
+ block: t.Optional[BlockIdExt] = None,
789
+ ) -> t.List[BlockIdExt]:
790
+ """
791
+ Fetch shard info for all workchains at a given masterchain block.
792
+
793
+ :param block: Masterchain block ID or None to use latest
794
+ :return: List of shard BlockIdExt objects
795
+ """
796
+
797
+ async def _call(provider: AdnlProvider) -> t.List[BlockIdExt]:
798
+ return await provider.get_all_shards_info(block)
799
+
800
+ return await self._with_failover(_call)