tonutils 2.0.1b1__py3-none-any.whl → 2.0.1b3__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 (79) hide show
  1. tonutils/__init__.py +0 -2
  2. tonutils/__meta__.py +1 -1
  3. tonutils/clients/__init__.py +5 -9
  4. tonutils/clients/adnl/__init__.py +5 -1
  5. tonutils/clients/adnl/balancer.py +319 -125
  6. tonutils/clients/adnl/client.py +187 -51
  7. tonutils/clients/adnl/provider/config.py +19 -25
  8. tonutils/clients/adnl/provider/models.py +4 -0
  9. tonutils/clients/adnl/provider/provider.py +191 -145
  10. tonutils/clients/adnl/provider/transport.py +38 -32
  11. tonutils/clients/adnl/provider/workers/base.py +0 -2
  12. tonutils/clients/adnl/provider/workers/pinger.py +1 -1
  13. tonutils/clients/adnl/provider/workers/reader.py +3 -2
  14. tonutils/clients/adnl/{provider/builder.py → utils.py} +62 -2
  15. tonutils/clients/http/__init__.py +11 -8
  16. tonutils/clients/http/balancer.py +75 -63
  17. tonutils/clients/http/clients/__init__.py +13 -0
  18. tonutils/clients/http/clients/chainstack.py +48 -0
  19. tonutils/clients/http/clients/quicknode.py +47 -0
  20. tonutils/clients/http/clients/tatum.py +56 -0
  21. tonutils/clients/http/{tonapi/client.py → clients/tonapi.py} +31 -31
  22. tonutils/clients/http/{toncenter/client.py → clients/toncenter.py} +59 -48
  23. tonutils/clients/http/providers/__init__.py +4 -0
  24. tonutils/clients/http/providers/base.py +201 -0
  25. tonutils/clients/http/providers/response.py +85 -0
  26. tonutils/clients/http/providers/tonapi/__init__.py +3 -0
  27. tonutils/clients/http/{tonapi → providers/tonapi}/models.py +1 -0
  28. tonutils/clients/http/providers/tonapi/provider.py +125 -0
  29. tonutils/clients/http/providers/toncenter/__init__.py +3 -0
  30. tonutils/clients/http/{toncenter → providers/toncenter}/models.py +1 -0
  31. tonutils/clients/http/providers/toncenter/provider.py +119 -0
  32. tonutils/clients/http/utils.py +140 -0
  33. tonutils/clients/limiter.py +115 -0
  34. tonutils/contracts/__init__.py +18 -0
  35. tonutils/contracts/base.py +33 -20
  36. tonutils/contracts/dns/methods.py +2 -2
  37. tonutils/contracts/jetton/methods.py +2 -2
  38. tonutils/contracts/nft/methods.py +2 -2
  39. tonutils/contracts/nft/tlb.py +1 -1
  40. tonutils/{protocols/contract.py → contracts/protocol.py} +29 -29
  41. tonutils/contracts/telegram/methods.py +2 -2
  42. tonutils/contracts/vanity/__init__.py +17 -0
  43. tonutils/contracts/vanity/models.py +39 -0
  44. tonutils/contracts/vanity/tlb.py +40 -0
  45. tonutils/contracts/vanity/vanity.py +40 -0
  46. tonutils/contracts/wallet/__init__.py +2 -0
  47. tonutils/contracts/wallet/base.py +3 -3
  48. tonutils/contracts/wallet/messages.py +1 -1
  49. tonutils/contracts/wallet/methods.py +2 -2
  50. tonutils/{protocols/wallet.py → contracts/wallet/protocol.py} +35 -35
  51. tonutils/contracts/wallet/versions/v5.py +3 -3
  52. tonutils/exceptions.py +134 -226
  53. tonutils/types.py +115 -0
  54. tonutils/utils.py +3 -3
  55. {tonutils-2.0.1b1.dist-info → tonutils-2.0.1b3.dist-info}/METADATA +4 -4
  56. tonutils-2.0.1b3.dist-info/RECORD +93 -0
  57. tonutils/clients/adnl/provider/limiter.py +0 -56
  58. tonutils/clients/adnl/stack.py +0 -64
  59. tonutils/clients/http/chainstack/__init__.py +0 -4
  60. tonutils/clients/http/chainstack/client.py +0 -63
  61. tonutils/clients/http/chainstack/provider.py +0 -44
  62. tonutils/clients/http/quicknode/__init__.py +0 -4
  63. tonutils/clients/http/quicknode/client.py +0 -60
  64. tonutils/clients/http/quicknode/provider.py +0 -42
  65. tonutils/clients/http/tatum/__init__.py +0 -4
  66. tonutils/clients/http/tatum/client.py +0 -66
  67. tonutils/clients/http/tatum/provider.py +0 -53
  68. tonutils/clients/http/tonapi/__init__.py +0 -4
  69. tonutils/clients/http/tonapi/provider.py +0 -150
  70. tonutils/clients/http/tonapi/stack.py +0 -71
  71. tonutils/clients/http/toncenter/__init__.py +0 -4
  72. tonutils/clients/http/toncenter/provider.py +0 -145
  73. tonutils/clients/http/toncenter/stack.py +0 -73
  74. tonutils/protocols/__init__.py +0 -9
  75. tonutils-2.0.1b1.dist-info/RECORD +0 -94
  76. /tonutils/{protocols/client.py → clients/protocol.py} +0 -0
  77. {tonutils-2.0.1b1.dist-info → tonutils-2.0.1b3.dist-info}/WHEEL +0 -0
  78. {tonutils-2.0.1b1.dist-info → tonutils-2.0.1b3.dist-info}/licenses/LICENSE +0 -0
  79. {tonutils-2.0.1b1.dist-info → tonutils-2.0.1b3.dist-info}/top_level.txt +0 -0
@@ -7,22 +7,34 @@ 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
12
  from tonutils.clients.adnl.client import AdnlClient
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
 
@@ -55,7 +67,8 @@ class AdnlBalancer(BaseClient):
55
67
  *,
56
68
  network: NetworkGlobalID = NetworkGlobalID.MAINNET,
57
69
  clients: t.List[AdnlClient],
58
- connect_timeout: int = 2,
70
+ connect_timeout: float = 2.0,
71
+ request_timeout: float = 12.0,
59
72
  ) -> None:
60
73
  """
61
74
  Initialize ADNL balancer.
@@ -70,7 +83,9 @@ class AdnlBalancer(BaseClient):
70
83
 
71
84
  :param network: Target TON network (mainnet or testnet)
72
85
  :param clients: List of AdnlClient instances to balance between
73
- :param connect_timeout: Timeout in seconds for connect/reconnect
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 providers
74
89
  """
75
90
  self.network: NetworkGlobalID = network
76
91
 
@@ -79,7 +94,9 @@ class AdnlBalancer(BaseClient):
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
@@ -109,6 +126,25 @@ class AdnlBalancer(BaseClient):
109
126
  self._clients.append(client)
110
127
  self._states.append(state)
111
128
 
129
+ @property
130
+ def provider(self) -> AdnlProvider:
131
+ """
132
+ Provider of the currently selected ADNL 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 ADNL 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
+
112
148
  @property
113
149
  def clients(self) -> t.Tuple[AdnlClient, ...]:
114
150
  """
@@ -148,28 +184,6 @@ 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
187
  async def __aenter__(self) -> AdnlBalancer:
174
188
  """
175
189
  Enter async context manager and connect underlying clients.
@@ -195,12 +209,14 @@ 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,
218
+ rps_per_client: bool = False,
219
+ retry_policy: t.Optional[RetryPolicy] = None,
204
220
  ) -> AdnlBalancer:
205
221
  """
206
222
  Build ADNL balancer from a lite-server config.
@@ -214,64 +230,71 @@ 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 ADNL client.
239
+ :param client_request_timeout: Timeout in seconds for a single request executed by an
240
+ individual ADNL 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
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
223
245
  :return: Configured AdnlBalancer 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)
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)
231
253
 
232
254
  clients: t.List[AdnlClient] = []
233
255
  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
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
+ AdnlClient(
265
+ network=network,
266
+ ip=node.host,
267
+ port=node.port,
268
+ public_key=node.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,
296
+ rps_per_client: bool = False,
297
+ retry_policy: t.Optional[RetryPolicy] = None,
275
298
  ) -> AdnlBalancer:
276
299
  """
277
300
  Build ADNL balancer using global config fetched from ton.org.
@@ -284,30 +307,36 @@ 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 ADNL client.
316
+ :param client_request_timeout: Timeout in seconds for a single request executed by an
317
+ individual ADNL 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
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
293
322
  :return: Configured AdnlBalancer 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
342
  def _pick_client(self) -> AdnlClient:
@@ -319,10 +348,10 @@ class AdnlBalancer(BaseClient):
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[
@@ -333,7 +362,7 @@ class AdnlBalancer(BaseClient):
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,10 +386,10 @@ 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
394
  def _mark_success(self, client: AdnlClient) -> None:
366
395
  """
@@ -413,45 +442,58 @@ class AdnlBalancer(BaseClient):
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
421
452
 
422
- client = self._pick_client()
453
+ client = self._pick_client()
454
+
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-server providers 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="adnl 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,6 +599,11 @@ class AdnlBalancer(BaseClient):
551
599
  )
552
600
 
553
601
  async def _health_loop(self) -> None:
602
+ """
603
+ Periodically attempt to reconnect dead ADNL clients.
604
+
605
+ Runs until cancelled.
606
+ """
554
607
 
555
608
  async def _recon(c: AdnlClient) -> None:
556
609
  with suppress(Exception):
@@ -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: int = -1,
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 seqno or -1 to ignore
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)