tonutils 2.0.1b5__py3-none-any.whl → 2.0.1b7__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/__meta__.py +1 -1
- tonutils/clients/__init__.py +10 -10
- tonutils/clients/adnl/balancer.py +135 -361
- tonutils/clients/adnl/client.py +35 -208
- tonutils/clients/adnl/mixin.py +268 -0
- tonutils/clients/adnl/provider/config.py +22 -7
- tonutils/clients/adnl/provider/provider.py +61 -16
- tonutils/clients/adnl/provider/transport.py +13 -4
- tonutils/clients/adnl/provider/workers/pinger.py +1 -1
- tonutils/clients/adnl/utils.py +5 -5
- tonutils/clients/base.py +61 -95
- tonutils/clients/http/__init__.py +11 -11
- tonutils/clients/http/balancer.py +103 -100
- tonutils/clients/http/clients/__init__.py +10 -10
- tonutils/clients/http/clients/chainstack.py +3 -3
- tonutils/clients/http/clients/quicknode.py +2 -3
- tonutils/clients/http/clients/tatum.py +4 -3
- tonutils/clients/http/clients/tonapi.py +20 -33
- tonutils/clients/http/clients/toncenter.py +64 -55
- tonutils/clients/http/{providers → provider}/__init__.py +4 -1
- tonutils/clients/http/{providers → provider}/base.py +140 -61
- tonutils/clients/http/{providers/toncenter → provider}/models.py +44 -2
- tonutils/clients/http/{providers/tonapi/provider.py → provider/tonapi.py} +8 -13
- tonutils/clients/http/{providers/toncenter/provider.py → provider/toncenter.py} +25 -21
- tonutils/clients/limiter.py +61 -59
- tonutils/clients/protocol.py +8 -8
- tonutils/contracts/base.py +32 -32
- tonutils/contracts/protocol.py +9 -9
- tonutils/contracts/wallet/base.py +7 -8
- tonutils/contracts/wallet/messages.py +4 -8
- tonutils/contracts/wallet/versions/v5.py +2 -2
- tonutils/exceptions.py +29 -13
- tonutils/tonconnect/bridge/__init__.py +0 -0
- tonutils/tonconnect/events.py +0 -0
- tonutils/tonconnect/models/__init__.py +0 -0
- tonutils/tonconnect/storage.py +0 -0
- tonutils/tonconnect/tonconnect.py +0 -0
- tonutils/tools/block_scanner/__init__.py +2 -5
- tonutils/tools/block_scanner/events.py +48 -7
- tonutils/tools/block_scanner/scanner.py +316 -222
- tonutils/tools/block_scanner/storage.py +11 -0
- tonutils/tools/status_monitor/monitor.py +6 -6
- tonutils/types.py +2 -2
- tonutils/utils.py +0 -48
- {tonutils-2.0.1b5.dist-info → tonutils-2.0.1b7.dist-info}/METADATA +3 -18
- {tonutils-2.0.1b5.dist-info → tonutils-2.0.1b7.dist-info}/RECORD +50 -51
- {tonutils-2.0.1b5.dist-info → tonutils-2.0.1b7.dist-info}/WHEEL +1 -1
- tonutils/clients/http/providers/response.py +0 -85
- tonutils/clients/http/providers/tonapi/__init__.py +0 -3
- tonutils/clients/http/providers/tonapi/models.py +0 -47
- tonutils/clients/http/providers/toncenter/__init__.py +0 -3
- tonutils/tools/block_scanner/annotations.py +0 -23
- tonutils/tools/block_scanner/dispatcher.py +0 -141
- tonutils/tools/block_scanner/traversal.py +0 -97
- tonutils/tools/block_scanner/where.py +0 -53
- {tonutils-2.0.1b5.dist-info → tonutils-2.0.1b7.dist-info}/entry_points.txt +0 -0
- {tonutils-2.0.1b5.dist-info → tonutils-2.0.1b7.dist-info}/licenses/LICENSE +0 -0
- {tonutils-2.0.1b5.dist-info → tonutils-2.0.1b7.dist-info}/top_level.txt +0 -0
tonutils/__meta__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2.0.
|
|
1
|
+
__version__ = "2.0.1b7"
|
tonutils/clients/__init__.py
CHANGED
|
@@ -5,12 +5,12 @@ from .adnl import (
|
|
|
5
5
|
)
|
|
6
6
|
from .http import (
|
|
7
7
|
HttpBalancer,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
ChainstackClient,
|
|
9
|
+
QuicknodeClient,
|
|
10
|
+
TatumClient,
|
|
11
|
+
TonapiClient,
|
|
12
12
|
TonapiHttpProvider,
|
|
13
|
-
|
|
13
|
+
ToncenterClient,
|
|
14
14
|
ToncenterHttpProvider,
|
|
15
15
|
)
|
|
16
16
|
|
|
@@ -19,11 +19,11 @@ __all__ = [
|
|
|
19
19
|
"LiteClient",
|
|
20
20
|
"AdnlProvider",
|
|
21
21
|
"HttpBalancer",
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
22
|
+
"ChainstackClient",
|
|
23
|
+
"QuicknodeClient",
|
|
24
|
+
"TatumClient",
|
|
25
|
+
"TonapiClient",
|
|
26
26
|
"TonapiHttpProvider",
|
|
27
|
-
"
|
|
27
|
+
"ToncenterClient",
|
|
28
28
|
"ToncenterHttpProvider",
|
|
29
29
|
]
|
|
@@ -7,22 +7,22 @@ from contextlib import suppress
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from itertools import cycle
|
|
9
9
|
|
|
10
|
-
from pytoniq_core import Address, BlockIdExt, Block, Transaction
|
|
11
|
-
|
|
12
10
|
from tonutils.clients.adnl.client import LiteClient
|
|
11
|
+
from tonutils.clients.adnl.mixin import LiteMixin
|
|
13
12
|
from tonutils.clients.adnl.provider import AdnlProvider
|
|
14
13
|
from tonutils.clients.adnl.provider.config import (
|
|
15
14
|
get_mainnet_global_config,
|
|
16
15
|
get_testnet_global_config,
|
|
16
|
+
load_global_config,
|
|
17
17
|
)
|
|
18
|
-
from tonutils.clients.adnl.provider.models import GlobalConfig
|
|
19
|
-
from tonutils.clients.adnl.utils import decode_stack, encode_stack
|
|
18
|
+
from tonutils.clients.adnl.provider.models import GlobalConfig
|
|
20
19
|
from tonutils.clients.base import BaseClient
|
|
21
20
|
from tonutils.clients.limiter import RateLimiter
|
|
22
21
|
from tonutils.exceptions import (
|
|
23
22
|
ClientError,
|
|
24
23
|
BalancerError,
|
|
25
24
|
RunGetMethodError,
|
|
25
|
+
NotConnectedError,
|
|
26
26
|
ProviderResponseError,
|
|
27
27
|
TransportError,
|
|
28
28
|
ProviderError,
|
|
@@ -30,10 +30,8 @@ from tonutils.exceptions import (
|
|
|
30
30
|
)
|
|
31
31
|
from tonutils.types import (
|
|
32
32
|
ClientType,
|
|
33
|
-
ContractStateInfo,
|
|
34
33
|
NetworkGlobalID,
|
|
35
34
|
RetryPolicy,
|
|
36
|
-
WorkchainID,
|
|
37
35
|
)
|
|
38
36
|
|
|
39
37
|
_T = t.TypeVar("_T")
|
|
@@ -52,7 +50,7 @@ class LiteClientState:
|
|
|
52
50
|
error_count: int = 0
|
|
53
51
|
|
|
54
52
|
|
|
55
|
-
class LiteBalancer(BaseClient):
|
|
53
|
+
class LiteBalancer(LiteMixin, BaseClient):
|
|
56
54
|
"""
|
|
57
55
|
Multi-client lite-server balancer with automatic failover and load balancing.
|
|
58
56
|
|
|
@@ -64,8 +62,8 @@ class LiteBalancer(BaseClient):
|
|
|
64
62
|
|
|
65
63
|
def __init__(
|
|
66
64
|
self,
|
|
67
|
-
*,
|
|
68
65
|
network: NetworkGlobalID = NetworkGlobalID.MAINNET,
|
|
66
|
+
*,
|
|
69
67
|
clients: t.List[LiteClient],
|
|
70
68
|
connect_timeout: float = 2.0,
|
|
71
69
|
request_timeout: float = 12.0,
|
|
@@ -91,7 +89,7 @@ class LiteBalancer(BaseClient):
|
|
|
91
89
|
|
|
92
90
|
self._clients: t.List[LiteClient] = []
|
|
93
91
|
self._states: t.List[LiteClientState] = []
|
|
94
|
-
self.
|
|
92
|
+
self._init_clients(clients)
|
|
95
93
|
|
|
96
94
|
self._rr = cycle(self._clients)
|
|
97
95
|
|
|
@@ -104,28 +102,6 @@ class LiteBalancer(BaseClient):
|
|
|
104
102
|
self._retry_after_base = 1.0
|
|
105
103
|
self._retry_after_max = 10.0
|
|
106
104
|
|
|
107
|
-
def __init_clients(
|
|
108
|
-
self,
|
|
109
|
-
clients: t.List[LiteClient],
|
|
110
|
-
) -> None:
|
|
111
|
-
"""
|
|
112
|
-
Validate and register input lite-server clients.
|
|
113
|
-
|
|
114
|
-
Ensures correct client type and network assignment.
|
|
115
|
-
"""
|
|
116
|
-
for client in clients:
|
|
117
|
-
if client.TYPE != ClientType.ADNL:
|
|
118
|
-
raise ClientError(
|
|
119
|
-
"LiteBalancer can work only with LiteClient instances, "
|
|
120
|
-
f"got {client.__class__.__name__}."
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
client.network = self.network
|
|
124
|
-
|
|
125
|
-
state = LiteClientState(client=client)
|
|
126
|
-
self._clients.append(client)
|
|
127
|
-
self._states.append(state)
|
|
128
|
-
|
|
129
105
|
@property
|
|
130
106
|
def provider(self) -> AdnlProvider:
|
|
131
107
|
"""
|
|
@@ -137,13 +113,13 @@ class LiteBalancer(BaseClient):
|
|
|
137
113
|
return c.provider
|
|
138
114
|
|
|
139
115
|
@property
|
|
140
|
-
def
|
|
116
|
+
def connected(self) -> bool:
|
|
141
117
|
"""
|
|
142
118
|
Check whether at least one underlying lite-server client is connected.
|
|
143
119
|
|
|
144
120
|
:return: True if any client is connected, otherwise False
|
|
145
121
|
"""
|
|
146
|
-
return any(c.
|
|
122
|
+
return any(c.connected for c in self._clients)
|
|
147
123
|
|
|
148
124
|
@property
|
|
149
125
|
def clients(self) -> t.Tuple[LiteClient, ...]:
|
|
@@ -165,7 +141,7 @@ class LiteBalancer(BaseClient):
|
|
|
165
141
|
return tuple(
|
|
166
142
|
state.client
|
|
167
143
|
for state in self._states
|
|
168
|
-
if state.client.
|
|
144
|
+
if state.client.connected
|
|
169
145
|
and (state.retry_after is None or state.retry_after <= now)
|
|
170
146
|
)
|
|
171
147
|
|
|
@@ -180,35 +156,16 @@ class LiteBalancer(BaseClient):
|
|
|
180
156
|
return tuple(
|
|
181
157
|
state.client
|
|
182
158
|
for state in self._states
|
|
183
|
-
if not state.client.
|
|
159
|
+
if not state.client.connected
|
|
184
160
|
or (state.retry_after is not None and state.retry_after > now)
|
|
185
161
|
)
|
|
186
162
|
|
|
187
|
-
async def __aenter__(self) -> LiteBalancer:
|
|
188
|
-
"""
|
|
189
|
-
Enter async context manager and connect underlying clients.
|
|
190
|
-
|
|
191
|
-
:return: Self instance with initialized connections
|
|
192
|
-
"""
|
|
193
|
-
await self.connect()
|
|
194
|
-
return self
|
|
195
|
-
|
|
196
|
-
async def __aexit__(
|
|
197
|
-
self,
|
|
198
|
-
exc_type: t.Optional[t.Type[BaseException]],
|
|
199
|
-
exc_value: t.Optional[BaseException],
|
|
200
|
-
traceback: t.Optional[t.Any],
|
|
201
|
-
) -> None:
|
|
202
|
-
"""Exit async context manager and close all underlying clients."""
|
|
203
|
-
with suppress(asyncio.CancelledError):
|
|
204
|
-
await self.close()
|
|
205
|
-
|
|
206
163
|
@classmethod
|
|
207
164
|
def from_config(
|
|
208
165
|
cls,
|
|
209
|
-
*,
|
|
210
166
|
network: NetworkGlobalID = NetworkGlobalID.MAINNET,
|
|
211
|
-
|
|
167
|
+
*,
|
|
168
|
+
config: t.Union[GlobalConfig, t.Dict[str, t.Any], str],
|
|
212
169
|
connect_timeout: float = 2.0,
|
|
213
170
|
request_timeout: float = 12.0,
|
|
214
171
|
client_connect_timeout: float = 1.5,
|
|
@@ -229,7 +186,7 @@ class LiteBalancer(BaseClient):
|
|
|
229
186
|
Public free configs may also be used via `from_network_config()`.
|
|
230
187
|
|
|
231
188
|
:param network: Target TON network
|
|
232
|
-
:param config: GlobalConfig instance or raw dict
|
|
189
|
+
:param config: GlobalConfig instance, config file path as string, or raw dict
|
|
233
190
|
:param connect_timeout: Timeout in seconds for a single connect/reconnect attempt
|
|
234
191
|
performed by the balancer during failover.
|
|
235
192
|
:param request_timeout: Maximum total time in seconds for a single balancer operation,
|
|
@@ -244,6 +201,8 @@ class LiteBalancer(BaseClient):
|
|
|
244
201
|
:param retry_policy: Optional retry policy that defines per-error-code retry rules
|
|
245
202
|
:return: Configured LiteBalancer instance
|
|
246
203
|
"""
|
|
204
|
+
if isinstance(config, str):
|
|
205
|
+
config = load_global_config(config)
|
|
247
206
|
if isinstance(config, dict):
|
|
248
207
|
config = GlobalConfig(**config)
|
|
249
208
|
|
|
@@ -285,8 +244,8 @@ class LiteBalancer(BaseClient):
|
|
|
285
244
|
@classmethod
|
|
286
245
|
def from_network_config(
|
|
287
246
|
cls,
|
|
288
|
-
*,
|
|
289
247
|
network: NetworkGlobalID = NetworkGlobalID.MAINNET,
|
|
248
|
+
*,
|
|
290
249
|
connect_timeout: float = 2.0,
|
|
291
250
|
request_timeout: float = 12.0,
|
|
292
251
|
client_connect_timeout: float = 1.5,
|
|
@@ -339,6 +298,60 @@ class LiteBalancer(BaseClient):
|
|
|
339
298
|
retry_policy=retry_policy,
|
|
340
299
|
)
|
|
341
300
|
|
|
301
|
+
async def connect(self) -> None:
|
|
302
|
+
if self.connected:
|
|
303
|
+
self._ensure_health_task()
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
async def _con(client: LiteClient) -> None:
|
|
307
|
+
with suppress(Exception):
|
|
308
|
+
await asyncio.wait_for(
|
|
309
|
+
client.connect(),
|
|
310
|
+
timeout=self._connect_timeout,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
tasks = [_con(client) for client in self._clients]
|
|
314
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
315
|
+
|
|
316
|
+
if self.connected:
|
|
317
|
+
self._ensure_health_task()
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
raise BalancerError("all lite-servers failed to establish connection")
|
|
321
|
+
|
|
322
|
+
async def close(self) -> None:
|
|
323
|
+
task, self._health_task = self._health_task, None
|
|
324
|
+
|
|
325
|
+
if task is not None and not task.done():
|
|
326
|
+
task.cancel()
|
|
327
|
+
with suppress(asyncio.CancelledError):
|
|
328
|
+
await task
|
|
329
|
+
|
|
330
|
+
tasks = [client.close() for client in self._clients]
|
|
331
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
332
|
+
|
|
333
|
+
def _init_clients(
|
|
334
|
+
self,
|
|
335
|
+
clients: t.List[LiteClient],
|
|
336
|
+
) -> None:
|
|
337
|
+
"""
|
|
338
|
+
Validate and register input lite-server clients.
|
|
339
|
+
|
|
340
|
+
Ensures correct client type and network assignment.
|
|
341
|
+
"""
|
|
342
|
+
for client in clients:
|
|
343
|
+
if client.TYPE != ClientType.ADNL:
|
|
344
|
+
raise ClientError(
|
|
345
|
+
f"{self.__class__.__name__} can work only with LiteClient instances, "
|
|
346
|
+
f"got {client.__class__.__name__}."
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
client.network = self.network
|
|
350
|
+
|
|
351
|
+
state = LiteClientState(client=client)
|
|
352
|
+
self._clients.append(client)
|
|
353
|
+
self._states.append(state)
|
|
354
|
+
|
|
342
355
|
def _pick_client(self) -> LiteClient:
|
|
343
356
|
"""
|
|
344
357
|
Select the best available lite-server client.
|
|
@@ -348,10 +361,13 @@ class LiteBalancer(BaseClient):
|
|
|
348
361
|
- minimal ping RTT and age among same-height clients
|
|
349
362
|
- round-robin fallback if no height information
|
|
350
363
|
"""
|
|
364
|
+
if not self.connected:
|
|
365
|
+
raise NotConnectedError(component=self.__class__.__name__)
|
|
366
|
+
|
|
351
367
|
alive = list(self.alive_clients)
|
|
352
368
|
|
|
353
369
|
if not alive:
|
|
354
|
-
raise BalancerError("no alive lite-
|
|
370
|
+
raise BalancerError("no alive lite-servers available")
|
|
355
371
|
|
|
356
372
|
height_candidates: t.List[
|
|
357
373
|
t.Tuple[
|
|
@@ -386,7 +402,7 @@ class LiteBalancer(BaseClient):
|
|
|
386
402
|
|
|
387
403
|
for _ in range(len(self._clients)):
|
|
388
404
|
candidate = next(self._rr)
|
|
389
|
-
if candidate in alive and candidate.
|
|
405
|
+
if candidate in alive and candidate.connected:
|
|
390
406
|
return candidate
|
|
391
407
|
|
|
392
408
|
return alive[0]
|
|
@@ -429,6 +445,48 @@ class LiteBalancer(BaseClient):
|
|
|
429
445
|
state.retry_after = now + cooldown
|
|
430
446
|
break
|
|
431
447
|
|
|
448
|
+
def _ensure_health_task(self) -> None:
|
|
449
|
+
"""
|
|
450
|
+
Ensure background health check task is running.
|
|
451
|
+
|
|
452
|
+
Starts a periodic reconnect loop for unavailable clients
|
|
453
|
+
if it is not already active.
|
|
454
|
+
"""
|
|
455
|
+
if self._health_task is not None and not self._health_task.done():
|
|
456
|
+
return
|
|
457
|
+
|
|
458
|
+
loop = asyncio.get_running_loop()
|
|
459
|
+
self._health_task = loop.create_task(
|
|
460
|
+
self._health_loop(),
|
|
461
|
+
name="_health_loop",
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
async def _health_loop(self) -> None:
|
|
465
|
+
"""
|
|
466
|
+
Periodically attempt to reconnect dead lite-server clients.
|
|
467
|
+
|
|
468
|
+
Runs until cancelled.
|
|
469
|
+
"""
|
|
470
|
+
|
|
471
|
+
async def _recon(c: LiteClient) -> None:
|
|
472
|
+
with suppress(Exception):
|
|
473
|
+
await asyncio.wait_for(
|
|
474
|
+
c.provider.reconnect(),
|
|
475
|
+
timeout=self._connect_timeout,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
try:
|
|
479
|
+
while True:
|
|
480
|
+
await asyncio.sleep(self._health_interval)
|
|
481
|
+
tasks = [
|
|
482
|
+
_recon(client)
|
|
483
|
+
for client in self.dead_clients
|
|
484
|
+
if not client.connected
|
|
485
|
+
]
|
|
486
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
487
|
+
except asyncio.CancelledError:
|
|
488
|
+
return
|
|
489
|
+
|
|
432
490
|
async def _with_failover(
|
|
433
491
|
self,
|
|
434
492
|
func: t.Callable[[AdnlProvider], t.Awaitable[_T]],
|
|
@@ -452,7 +510,7 @@ class LiteBalancer(BaseClient):
|
|
|
452
510
|
|
|
453
511
|
client = self._pick_client()
|
|
454
512
|
|
|
455
|
-
if not client.provider.
|
|
513
|
+
if not client.provider.connected:
|
|
456
514
|
try:
|
|
457
515
|
await asyncio.wait_for(
|
|
458
516
|
client.provider.reconnect(),
|
|
@@ -482,319 +540,35 @@ class LiteBalancer(BaseClient):
|
|
|
482
540
|
return result
|
|
483
541
|
|
|
484
542
|
if last_exc is not None:
|
|
485
|
-
raise last_exc
|
|
486
|
-
|
|
487
|
-
raise BalancerError("all lite-servers failed to process request")
|
|
543
|
+
raise BalancerError("lite failover exhausted") from last_exc
|
|
544
|
+
raise BalancerError("no alive lite-servers available")
|
|
488
545
|
|
|
489
546
|
try:
|
|
490
547
|
return await asyncio.wait_for(_run(), timeout=self._request_timeout)
|
|
491
548
|
except asyncio.TimeoutError as exc:
|
|
492
549
|
raise ProviderTimeoutError(
|
|
493
550
|
timeout=self._request_timeout,
|
|
494
|
-
endpoint=
|
|
551
|
+
endpoint=self.__class__.__name__,
|
|
495
552
|
operation="failover request",
|
|
496
553
|
) from exc
|
|
497
554
|
|
|
498
|
-
async def
|
|
499
|
-
async def _call(provider: AdnlProvider) -> None:
|
|
500
|
-
return await provider.send_message(bytes.fromhex(boc))
|
|
501
|
-
|
|
502
|
-
return await self._with_failover(_call)
|
|
503
|
-
|
|
504
|
-
async def _get_blockchain_config(self) -> t.Dict[int, t.Any]:
|
|
505
|
-
async def _call(provider: AdnlProvider) -> t.Dict[int, t.Any]:
|
|
506
|
-
return await provider.get_config_all()
|
|
507
|
-
|
|
508
|
-
return await self._with_failover(_call)
|
|
509
|
-
|
|
510
|
-
async def _get_contract_info(self, address: str) -> ContractStateInfo:
|
|
511
|
-
async def _call(provider: AdnlProvider) -> ContractStateInfo:
|
|
512
|
-
return await provider.get_account_state(Address(address))
|
|
513
|
-
|
|
514
|
-
return await self._with_failover(_call)
|
|
515
|
-
|
|
516
|
-
async def _get_contract_transactions(
|
|
517
|
-
self,
|
|
518
|
-
address: str,
|
|
519
|
-
limit: int = 100,
|
|
520
|
-
from_lt: t.Optional[int] = None,
|
|
521
|
-
to_lt: int = 0,
|
|
522
|
-
) -> t.List[Transaction]:
|
|
523
|
-
state = await self._get_contract_info(address)
|
|
524
|
-
account = Address(address).to_tl_account_id()
|
|
525
|
-
|
|
526
|
-
if state.last_transaction_lt is None or state.last_transaction_hash is None:
|
|
527
|
-
return []
|
|
528
|
-
|
|
529
|
-
curr_lt = state.last_transaction_lt
|
|
530
|
-
curr_hash = state.last_transaction_hash
|
|
531
|
-
transactions: t.List[Transaction] = []
|
|
532
|
-
|
|
533
|
-
while len(transactions) < limit and curr_lt != 0:
|
|
534
|
-
batch_size = min(16, limit - len(transactions))
|
|
535
|
-
|
|
536
|
-
async def _call(provider: AdnlProvider) -> t.List[Transaction]:
|
|
537
|
-
return await provider.get_transactions(
|
|
538
|
-
account=account,
|
|
539
|
-
count=batch_size,
|
|
540
|
-
from_lt=curr_lt,
|
|
541
|
-
from_hash=curr_hash,
|
|
542
|
-
)
|
|
543
|
-
|
|
544
|
-
txs = await self._with_failover(_call)
|
|
545
|
-
if not txs:
|
|
546
|
-
break
|
|
547
|
-
|
|
548
|
-
if to_lt > 0 and txs[-1].lt <= to_lt:
|
|
549
|
-
trimmed: t.List[Transaction] = []
|
|
550
|
-
for tx in txs:
|
|
551
|
-
if tx.lt <= to_lt:
|
|
552
|
-
break
|
|
553
|
-
trimmed.append(tx)
|
|
554
|
-
transactions.extend(trimmed)
|
|
555
|
-
break
|
|
556
|
-
|
|
557
|
-
transactions.extend(txs)
|
|
558
|
-
|
|
559
|
-
last_tx = txs[-1]
|
|
560
|
-
curr_lt = last_tx.prev_trans_lt
|
|
561
|
-
curr_hash = last_tx.prev_trans_hash.hex()
|
|
562
|
-
|
|
563
|
-
return (
|
|
564
|
-
[tx for tx in transactions if tx.lt < from_lt]
|
|
565
|
-
if from_lt is not None
|
|
566
|
-
else transactions
|
|
567
|
-
)
|
|
568
|
-
|
|
569
|
-
async def _run_get_method(
|
|
570
|
-
self,
|
|
571
|
-
address: str,
|
|
572
|
-
method_name: str,
|
|
573
|
-
stack: t.Optional[t.List[t.Any]] = None,
|
|
574
|
-
) -> t.List[t.Any]:
|
|
575
|
-
async def _call(provider: AdnlProvider) -> t.List[t.Any]:
|
|
576
|
-
result = await provider.run_smc_method(
|
|
577
|
-
address=Address(address),
|
|
578
|
-
method_name=method_name,
|
|
579
|
-
stack=encode_stack(stack or []),
|
|
580
|
-
)
|
|
581
|
-
return decode_stack(result or [])
|
|
582
|
-
|
|
583
|
-
return await self._with_failover(_call)
|
|
584
|
-
|
|
585
|
-
def _ensure_health_task(self) -> None:
|
|
555
|
+
async def _adnl_call(self, method: str, /, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
586
556
|
"""
|
|
587
|
-
|
|
557
|
+
Execute lite-server call with failover across providers.
|
|
588
558
|
|
|
589
|
-
|
|
590
|
-
|
|
559
|
+
:param method: Provider coroutine method name.
|
|
560
|
+
:param args: Positional arguments forwarded to the provider method.
|
|
561
|
+
:param kwargs: Keyword arguments forwarded to the provider method.
|
|
562
|
+
:return: Provider method result.
|
|
591
563
|
"""
|
|
592
|
-
if
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
self._health_task = loop.create_task(
|
|
597
|
-
self._health_loop(),
|
|
598
|
-
name="_health_loop",
|
|
599
|
-
)
|
|
600
|
-
|
|
601
|
-
async def _health_loop(self) -> None:
|
|
602
|
-
"""
|
|
603
|
-
Periodically attempt to reconnect dead lite-server clients.
|
|
604
|
-
|
|
605
|
-
Runs until cancelled.
|
|
606
|
-
"""
|
|
607
|
-
|
|
608
|
-
async def _recon(c: LiteClient) -> None:
|
|
609
|
-
with suppress(Exception):
|
|
610
|
-
await asyncio.wait_for(
|
|
611
|
-
c.reconnect(),
|
|
612
|
-
timeout=self._connect_timeout,
|
|
613
|
-
)
|
|
614
|
-
|
|
615
|
-
try:
|
|
616
|
-
while True:
|
|
617
|
-
await asyncio.sleep(self._health_interval)
|
|
618
|
-
tasks = [
|
|
619
|
-
_recon(client)
|
|
620
|
-
for client in self.dead_clients
|
|
621
|
-
if not client.is_connected
|
|
622
|
-
]
|
|
623
|
-
await asyncio.gather(*tasks, return_exceptions=True)
|
|
624
|
-
except asyncio.CancelledError:
|
|
625
|
-
return
|
|
626
|
-
|
|
627
|
-
async def connect(self) -> None:
|
|
628
|
-
if self.is_connected:
|
|
629
|
-
self._ensure_health_task()
|
|
630
|
-
return
|
|
631
|
-
|
|
632
|
-
async def _con(client: LiteClient) -> None:
|
|
633
|
-
with suppress(asyncio.TimeoutError):
|
|
634
|
-
await asyncio.wait_for(
|
|
635
|
-
client.connect(),
|
|
636
|
-
timeout=self._connect_timeout,
|
|
637
|
-
)
|
|
638
|
-
|
|
639
|
-
tasks = [_con(client) for client in self._clients]
|
|
640
|
-
await asyncio.gather(*tasks, return_exceptions=True)
|
|
641
|
-
|
|
642
|
-
if self.is_connected:
|
|
643
|
-
self._ensure_health_task()
|
|
644
|
-
return
|
|
645
|
-
|
|
646
|
-
raise BalancerError("all lite-servers failed to establish connection")
|
|
647
|
-
|
|
648
|
-
async def close(self) -> None:
|
|
649
|
-
task, self._health_task = self._health_task, None
|
|
650
|
-
|
|
651
|
-
if task is not None and not task.done():
|
|
652
|
-
task.cancel()
|
|
653
|
-
with suppress(asyncio.CancelledError):
|
|
654
|
-
await task
|
|
655
|
-
|
|
656
|
-
tasks = [client.close() for client in self._clients]
|
|
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,
|
|
564
|
+
if not self.connected:
|
|
565
|
+
raise NotConnectedError(
|
|
566
|
+
component=self.__class__.__name__,
|
|
567
|
+
operation=method,
|
|
748
568
|
)
|
|
749
569
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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)
|
|
570
|
+
async def _call(provider: AdnlProvider) -> t.Any:
|
|
571
|
+
fn = getattr(provider, method)
|
|
572
|
+
return await fn(*args, **kwargs)
|
|
799
573
|
|
|
800
574
|
return await self._with_failover(_call)
|