tonutils 2.0.1b2__py3-none-any.whl → 2.0.1b4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tonutils/__init__.py +0 -2
- tonutils/__meta__.py +1 -1
- tonutils/cli.py +111 -0
- tonutils/clients/__init__.py +7 -11
- tonutils/clients/adnl/__init__.py +7 -3
- tonutils/clients/adnl/balancer.py +362 -168
- tonutils/clients/adnl/client.py +203 -67
- tonutils/clients/adnl/provider/config.py +24 -25
- tonutils/clients/adnl/provider/models.py +4 -0
- tonutils/clients/adnl/provider/provider.py +203 -160
- tonutils/clients/adnl/provider/transport.py +44 -33
- tonutils/clients/adnl/provider/workers/base.py +0 -2
- tonutils/clients/adnl/provider/workers/pinger.py +1 -1
- tonutils/clients/adnl/provider/workers/reader.py +3 -2
- tonutils/clients/adnl/{provider/builder.py → utils.py} +62 -2
- tonutils/clients/http/__init__.py +11 -8
- tonutils/clients/http/balancer.py +75 -63
- tonutils/clients/http/clients/__init__.py +13 -0
- tonutils/clients/http/clients/chainstack.py +48 -0
- tonutils/clients/http/clients/quicknode.py +47 -0
- tonutils/clients/http/clients/tatum.py +56 -0
- tonutils/clients/http/{tonapi/client.py → clients/tonapi.py} +31 -31
- tonutils/clients/http/{toncenter/client.py → clients/toncenter.py} +59 -48
- tonutils/clients/http/providers/__init__.py +4 -0
- tonutils/clients/http/providers/base.py +201 -0
- tonutils/clients/http/providers/response.py +85 -0
- tonutils/clients/http/providers/tonapi/__init__.py +3 -0
- tonutils/clients/http/{tonapi → providers/tonapi}/models.py +1 -0
- tonutils/clients/http/providers/tonapi/provider.py +125 -0
- tonutils/clients/http/providers/toncenter/__init__.py +3 -0
- tonutils/clients/http/{toncenter → providers/toncenter}/models.py +1 -0
- tonutils/clients/http/providers/toncenter/provider.py +119 -0
- tonutils/clients/http/utils.py +140 -0
- tonutils/clients/limiter.py +115 -0
- tonutils/contracts/__init__.py +4 -0
- tonutils/contracts/base.py +33 -20
- tonutils/contracts/dns/methods.py +2 -2
- tonutils/contracts/jetton/methods.py +2 -2
- tonutils/contracts/nft/methods.py +2 -2
- tonutils/contracts/nft/tlb.py +1 -1
- tonutils/{protocols/contract.py → contracts/protocol.py} +29 -29
- tonutils/contracts/telegram/methods.py +2 -2
- tonutils/contracts/vanity/vanity.py +1 -1
- tonutils/contracts/wallet/__init__.py +2 -0
- tonutils/contracts/wallet/base.py +3 -3
- tonutils/contracts/wallet/messages.py +1 -1
- tonutils/contracts/wallet/methods.py +2 -2
- tonutils/{protocols/wallet.py → contracts/wallet/protocol.py} +35 -35
- tonutils/contracts/wallet/versions/v5.py +3 -3
- tonutils/exceptions.py +146 -228
- tonutils/tonconnect/__init__.py +0 -0
- tonutils/tools/__init__.py +6 -0
- tonutils/tools/block_scanner/__init__.py +26 -0
- tonutils/tools/block_scanner/annotations.py +23 -0
- tonutils/tools/block_scanner/dispatcher.py +141 -0
- tonutils/tools/block_scanner/events.py +31 -0
- tonutils/tools/block_scanner/scanner.py +315 -0
- tonutils/tools/block_scanner/traversal.py +96 -0
- tonutils/tools/block_scanner/where.py +151 -0
- tonutils/tools/status_monitor/__init__.py +3 -0
- tonutils/tools/status_monitor/console.py +157 -0
- tonutils/tools/status_monitor/models.py +27 -0
- tonutils/tools/status_monitor/monitor.py +295 -0
- tonutils/types.py +125 -2
- tonutils/utils.py +3 -3
- {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b4.dist-info}/METADATA +2 -5
- tonutils-2.0.1b4.dist-info/RECORD +108 -0
- tonutils-2.0.1b4.dist-info/entry_points.txt +2 -0
- tonutils/clients/adnl/provider/limiter.py +0 -56
- tonutils/clients/adnl/stack.py +0 -64
- tonutils/clients/http/chainstack/__init__.py +0 -4
- tonutils/clients/http/chainstack/client.py +0 -63
- tonutils/clients/http/chainstack/provider.py +0 -44
- tonutils/clients/http/quicknode/__init__.py +0 -4
- tonutils/clients/http/quicknode/client.py +0 -60
- tonutils/clients/http/quicknode/provider.py +0 -42
- tonutils/clients/http/tatum/__init__.py +0 -4
- tonutils/clients/http/tatum/client.py +0 -66
- tonutils/clients/http/tatum/provider.py +0 -53
- tonutils/clients/http/tonapi/__init__.py +0 -4
- tonutils/clients/http/tonapi/provider.py +0 -150
- tonutils/clients/http/tonapi/stack.py +0 -71
- tonutils/clients/http/toncenter/__init__.py +0 -4
- tonutils/clients/http/toncenter/provider.py +0 -145
- tonutils/clients/http/toncenter/stack.py +0 -73
- tonutils/protocols/__init__.py +0 -9
- tonutils-2.0.1b2.dist-info/RECORD +0 -98
- /tonutils/{protocols/client.py → clients/protocol.py} +0 -0
- {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b4.dist-info}/WHEEL +0 -0
- {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b4.dist-info}/licenses/LICENSE +0 -0
- {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b4.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
43
|
+
class LiteClientState:
|
|
32
44
|
"""
|
|
33
|
-
Internal state container for
|
|
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:
|
|
50
|
+
client: LiteClient
|
|
39
51
|
retry_after: t.Optional[float] = None
|
|
40
52
|
error_count: int = 0
|
|
41
53
|
|
|
42
54
|
|
|
43
|
-
class
|
|
55
|
+
class LiteBalancer(BaseClient):
|
|
44
56
|
"""
|
|
45
|
-
Multi-
|
|
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[
|
|
58
|
-
connect_timeout:
|
|
69
|
+
clients: t.List[LiteClient],
|
|
70
|
+
connect_timeout: float = 2.0,
|
|
71
|
+
request_timeout: float = 12.0,
|
|
59
72
|
) -> None:
|
|
60
73
|
"""
|
|
61
|
-
Initialize
|
|
74
|
+
Initialize lite-server balancer.
|
|
62
75
|
|
|
63
|
-
It is recommended to build underlying
|
|
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
|
|
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[
|
|
78
|
-
self._states: t.List[
|
|
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[
|
|
109
|
+
clients: t.List[LiteClient],
|
|
93
110
|
) -> None:
|
|
94
111
|
"""
|
|
95
|
-
Validate and register input
|
|
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
|
-
"
|
|
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 =
|
|
125
|
+
state = LiteClientState(client=client)
|
|
109
126
|
self._clients.append(client)
|
|
110
127
|
self._states.append(state)
|
|
111
128
|
|
|
112
129
|
@property
|
|
113
|
-
def
|
|
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
|
|
151
|
+
List of all registered lite-server clients.
|
|
116
152
|
|
|
117
|
-
:return: Tuple of
|
|
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[
|
|
158
|
+
def alive_clients(self) -> t.Tuple[LiteClient, ...]:
|
|
123
159
|
"""
|
|
124
|
-
|
|
160
|
+
Lite-server clients that are allowed to send requests now.
|
|
125
161
|
|
|
126
|
-
:return: Tuple of available
|
|
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[
|
|
173
|
+
def dead_clients(self) -> t.Tuple[LiteClient, ...]:
|
|
138
174
|
"""
|
|
139
|
-
|
|
175
|
+
Lite-server clients currently in cooldown or disconnected.
|
|
140
176
|
|
|
141
|
-
:return: Tuple of unavailable
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
) ->
|
|
218
|
+
rps_per_client: bool = False,
|
|
219
|
+
retry_policy: t.Optional[RetryPolicy] = None,
|
|
220
|
+
) -> LiteBalancer:
|
|
205
221
|
"""
|
|
206
|
-
Build
|
|
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
|
|
218
|
-
|
|
219
|
-
:param
|
|
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
|
|
222
|
-
:param
|
|
223
|
-
:return: Configured
|
|
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[
|
|
229
|
-
if rps_limit is not None and not
|
|
230
|
-
shared_limiter =
|
|
231
|
-
|
|
232
|
-
clients: t.List[
|
|
233
|
-
for
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
286
|
+
def from_network_config(
|
|
266
287
|
cls,
|
|
267
288
|
*,
|
|
268
289
|
network: NetworkGlobalID = NetworkGlobalID.MAINNET,
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
) ->
|
|
296
|
+
rps_per_client: bool = False,
|
|
297
|
+
retry_policy: t.Optional[RetryPolicy] = None,
|
|
298
|
+
) -> LiteBalancer:
|
|
276
299
|
"""
|
|
277
|
-
Build
|
|
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
|
|
288
|
-
|
|
289
|
-
:param
|
|
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
|
|
292
|
-
:param
|
|
293
|
-
:return: Configured
|
|
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:
|
|
298
|
-
NetworkGlobalID.TESTNET:
|
|
325
|
+
NetworkGlobalID.MAINNET: get_mainnet_global_config,
|
|
326
|
+
NetworkGlobalID.TESTNET: get_testnet_global_config,
|
|
299
327
|
}
|
|
300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
309
|
-
|
|
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) ->
|
|
342
|
+
def _pick_client(self) -> LiteClient:
|
|
314
343
|
"""
|
|
315
|
-
Select the best available
|
|
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
|
-
|
|
351
|
+
alive = list(self.alive_clients)
|
|
323
352
|
|
|
324
|
-
if not
|
|
325
|
-
raise
|
|
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
|
-
|
|
361
|
+
LiteClient,
|
|
333
362
|
]
|
|
334
363
|
] = []
|
|
335
364
|
|
|
336
|
-
for client in
|
|
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
|
|
389
|
+
if candidate in alive and candidate.is_connected:
|
|
361
390
|
return candidate
|
|
362
391
|
|
|
363
|
-
return
|
|
392
|
+
return alive[0]
|
|
364
393
|
|
|
365
|
-
def _mark_success(self, client:
|
|
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:
|
|
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
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
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
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
except
|
|
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
|
-
|
|
436
|
-
result
|
|
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
|
-
|
|
447
|
-
|
|
484
|
+
if last_exc is not None:
|
|
485
|
+
raise last_exc
|
|
448
486
|
|
|
449
|
-
|
|
450
|
-
raise last_exc
|
|
487
|
+
raise BalancerError("all lite-servers failed to process request")
|
|
451
488
|
|
|
452
|
-
|
|
453
|
-
|
|
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:
|
|
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) ->
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
|
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)
|