tonutils 2.0.1b6__py3-none-any.whl → 2.0.1b8__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 +8 -13
- tonutils/cli.py +1 -1
- tonutils/clients/adnl/balancer.py +132 -355
- tonutils/clients/adnl/client.py +32 -202
- tonutils/clients/adnl/mixin.py +268 -0
- tonutils/clients/adnl/provider/config.py +7 -20
- 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 +52 -92
- tonutils/clients/http/balancer.py +93 -90
- tonutils/clients/http/clients/tatum.py +1 -0
- tonutils/clients/http/clients/tonapi.py +12 -24
- tonutils/clients/http/clients/toncenter.py +15 -33
- tonutils/clients/http/provider/base.py +75 -60
- tonutils/clients/http/provider/models.py +1 -1
- tonutils/clients/http/provider/tonapi.py +0 -5
- tonutils/clients/http/provider/toncenter.py +4 -8
- tonutils/clients/protocol.py +6 -6
- tonutils/contracts/__init__.py +3 -0
- tonutils/contracts/base.py +32 -32
- tonutils/contracts/protocol.py +9 -9
- tonutils/contracts/telegram/tlb.py +1 -1
- tonutils/contracts/wallet/__init__.py +4 -0
- tonutils/contracts/wallet/base.py +5 -5
- tonutils/contracts/wallet/tlb.py +18 -16
- tonutils/contracts/wallet/versions/v5.py +6 -6
- tonutils/exceptions.py +45 -102
- tonutils/tools/block_scanner/__init__.py +5 -1
- tonutils/tools/block_scanner/scanner.py +1 -1
- tonutils/tools/status_monitor/monitor.py +6 -6
- tonutils/types.py +24 -10
- tonutils/utils.py +47 -7
- {tonutils-2.0.1b6.dist-info → tonutils-2.0.1b8.dist-info}/METADATA +3 -20
- {tonutils-2.0.1b6.dist-info → tonutils-2.0.1b8.dist-info}/RECORD +40 -46
- {tonutils-2.0.1b6.dist-info → tonutils-2.0.1b8.dist-info}/WHEEL +1 -1
- tonutils/__meta__.py +0 -1
- tonutils/tonconnect/__init__.py +0 -0
- 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-2.0.1b6.dist-info → tonutils-2.0.1b8.dist-info}/entry_points.txt +0 -0
- {tonutils-2.0.1b6.dist-info → tonutils-2.0.1b8.dist-info}/licenses/LICENSE +0 -0
- {tonutils-2.0.1b6.dist-info → tonutils-2.0.1b8.dist-info}/top_level.txt +0 -0
|
@@ -19,8 +19,9 @@ from tonutils.exceptions import (
|
|
|
19
19
|
ProviderResponseError,
|
|
20
20
|
RunGetMethodError,
|
|
21
21
|
ProviderTimeoutError,
|
|
22
|
+
NotConnectedError,
|
|
22
23
|
)
|
|
23
|
-
from tonutils.types import ClientType,
|
|
24
|
+
from tonutils.types import ClientType, ContractInfo, NetworkGlobalID
|
|
24
25
|
|
|
25
26
|
_T = t.TypeVar("_T")
|
|
26
27
|
|
|
@@ -73,14 +74,14 @@ class HttpBalancer(BaseClient):
|
|
|
73
74
|
|
|
74
75
|
:param network: Target TON network (mainnet or testnet)
|
|
75
76
|
:param clients: List of HTTP BaseClient instances to balance between
|
|
76
|
-
:param request_timeout: Maximum total time in seconds for a balancer
|
|
77
|
+
:param request_timeout: Maximum total time in seconds for a balancer method,
|
|
77
78
|
including all failover attempts across providers
|
|
78
79
|
"""
|
|
79
80
|
self.network = network
|
|
80
81
|
|
|
81
82
|
self._clients: t.List[BaseClient] = []
|
|
82
83
|
self._states: t.List[HttpClientState] = []
|
|
83
|
-
self.
|
|
84
|
+
self._init_clients(clients)
|
|
84
85
|
|
|
85
86
|
self._rr = cycle(self._clients)
|
|
86
87
|
|
|
@@ -89,31 +90,14 @@ class HttpBalancer(BaseClient):
|
|
|
89
90
|
|
|
90
91
|
self._request_timeout = request_timeout
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
"""Validate and register input HTTP clients."""
|
|
97
|
-
for client in clients:
|
|
98
|
-
if client.TYPE != ClientType.HTTP:
|
|
99
|
-
raise ClientError(
|
|
100
|
-
"HttpBalancer can work only with HTTP clients, "
|
|
101
|
-
f"got {client.__class__.__name__}."
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
isinstance(client, QuicknodeClient)
|
|
106
|
-
and self.network == NetworkGlobalID.TESTNET
|
|
107
|
-
):
|
|
108
|
-
raise ClientError(
|
|
109
|
-
"QuickNode HTTP client does not support testnet network."
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
client.network = self.network
|
|
113
|
-
state = HttpClientState(client=client)
|
|
93
|
+
@property
|
|
94
|
+
def connected(self) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
Check whether at least one underlying client is connected.
|
|
114
97
|
|
|
115
|
-
|
|
116
|
-
|
|
98
|
+
:return: True if any client is connected, otherwise False
|
|
99
|
+
"""
|
|
100
|
+
return any(c.connected for c in self._clients)
|
|
117
101
|
|
|
118
102
|
@property
|
|
119
103
|
def provider(self) -> t.Any:
|
|
@@ -125,14 +109,17 @@ class HttpBalancer(BaseClient):
|
|
|
125
109
|
c = self._pick_client()
|
|
126
110
|
return c.provider
|
|
127
111
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
112
|
+
async def connect(self) -> None:
|
|
113
|
+
await asyncio.gather(
|
|
114
|
+
*(state.client.connect() for state in self._states),
|
|
115
|
+
return_exceptions=True,
|
|
116
|
+
)
|
|
132
117
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
118
|
+
async def close(self) -> None:
|
|
119
|
+
await asyncio.gather(
|
|
120
|
+
*(state.client.close() for state in self._states),
|
|
121
|
+
return_exceptions=True,
|
|
122
|
+
)
|
|
136
123
|
|
|
137
124
|
@property
|
|
138
125
|
def clients(self) -> t.Tuple[BaseClient, ...]:
|
|
@@ -154,7 +141,8 @@ class HttpBalancer(BaseClient):
|
|
|
154
141
|
return tuple(
|
|
155
142
|
state.client
|
|
156
143
|
for state in self._states
|
|
157
|
-
if state.
|
|
144
|
+
if state.client.connected
|
|
145
|
+
and (state.retry_after is None or state.retry_after <= now)
|
|
158
146
|
)
|
|
159
147
|
|
|
160
148
|
@property
|
|
@@ -171,24 +159,31 @@ class HttpBalancer(BaseClient):
|
|
|
171
159
|
if state.retry_after is not None and state.retry_after > now
|
|
172
160
|
)
|
|
173
161
|
|
|
174
|
-
|
|
175
|
-
"""
|
|
176
|
-
Enter async context manager and connect all underlying clients.
|
|
177
|
-
|
|
178
|
-
:return: Self instance with initialized connections
|
|
179
|
-
"""
|
|
180
|
-
await self.connect()
|
|
181
|
-
return self
|
|
182
|
-
|
|
183
|
-
async def __aexit__(
|
|
162
|
+
def _init_clients(
|
|
184
163
|
self,
|
|
185
|
-
|
|
186
|
-
exc_value: t.Optional[BaseException],
|
|
187
|
-
traceback: t.Optional[t.Any],
|
|
164
|
+
clients: t.List[BaseClient],
|
|
188
165
|
) -> None:
|
|
189
|
-
"""
|
|
190
|
-
|
|
191
|
-
|
|
166
|
+
"""Validate and register input HTTP clients."""
|
|
167
|
+
for client in clients:
|
|
168
|
+
if client.TYPE != ClientType.HTTP:
|
|
169
|
+
raise ClientError(
|
|
170
|
+
"HttpBalancer can work only with HTTP clients, "
|
|
171
|
+
f"got {client.__class__.__name__}."
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if (
|
|
175
|
+
isinstance(client, QuicknodeClient)
|
|
176
|
+
and self.network == NetworkGlobalID.TESTNET
|
|
177
|
+
):
|
|
178
|
+
raise ClientError(
|
|
179
|
+
"QuickNode HTTP client does not support testnet network."
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
client.network = self.network
|
|
183
|
+
state = HttpClientState(client=client)
|
|
184
|
+
|
|
185
|
+
self._clients.append(client)
|
|
186
|
+
self._states.append(state)
|
|
192
187
|
|
|
193
188
|
def _pick_client(self) -> BaseClient:
|
|
194
189
|
"""
|
|
@@ -201,8 +196,10 @@ class HttpBalancer(BaseClient):
|
|
|
201
196
|
|
|
202
197
|
:return: Selected BaseClient instance
|
|
203
198
|
"""
|
|
204
|
-
|
|
199
|
+
if not self.connected:
|
|
200
|
+
raise NotConnectedError(component=self.__class__.__name__)
|
|
205
201
|
|
|
202
|
+
alive = list(self.alive_clients)
|
|
206
203
|
height_candidates: t.List[
|
|
207
204
|
t.Tuple[
|
|
208
205
|
float,
|
|
@@ -223,7 +220,9 @@ class HttpBalancer(BaseClient):
|
|
|
223
220
|
height_candidates.append((wait, state.error_count, state))
|
|
224
221
|
|
|
225
222
|
if not height_candidates:
|
|
226
|
-
raise BalancerError(
|
|
223
|
+
raise BalancerError(
|
|
224
|
+
"http balancer has no available clients (all in cooldown or not connected)"
|
|
225
|
+
)
|
|
227
226
|
|
|
228
227
|
height_candidates.sort(key=lambda x: (x[0], x[1]))
|
|
229
228
|
best_wait, best_err, _ = height_candidates[0]
|
|
@@ -286,75 +285,89 @@ class HttpBalancer(BaseClient):
|
|
|
286
285
|
async def _with_failover(
|
|
287
286
|
self,
|
|
288
287
|
func: t.Callable[[BaseClient], t.Awaitable[_T]],
|
|
288
|
+
method: str,
|
|
289
289
|
) -> _T:
|
|
290
290
|
"""
|
|
291
|
-
Execute a client
|
|
291
|
+
Execute a client method with automatic failover.
|
|
292
292
|
|
|
293
293
|
Iterates through available clients until one succeeds
|
|
294
294
|
or all clients fail.
|
|
295
295
|
|
|
296
|
-
:param func: Callable performing an
|
|
296
|
+
:param func: Callable performing an method using a client
|
|
297
297
|
:return: Result of the successful invocation
|
|
298
298
|
"""
|
|
299
299
|
|
|
300
300
|
async def _run() -> _T:
|
|
301
|
+
if not self.connected:
|
|
302
|
+
raise NotConnectedError(
|
|
303
|
+
component=self.__class__.__name__,
|
|
304
|
+
operation=method,
|
|
305
|
+
)
|
|
306
|
+
|
|
301
307
|
last_exc: t.Optional[BaseException] = None
|
|
308
|
+
attempts = 0
|
|
302
309
|
|
|
303
310
|
for _ in range(len(self._clients)):
|
|
304
311
|
if not self.alive_clients:
|
|
305
312
|
break
|
|
306
313
|
|
|
307
314
|
client = self._pick_client()
|
|
315
|
+
attempts += 1
|
|
308
316
|
|
|
309
317
|
try:
|
|
310
318
|
result = await func(client)
|
|
311
|
-
|
|
312
319
|
except RunGetMethodError:
|
|
313
320
|
raise
|
|
314
321
|
except ProviderResponseError as e:
|
|
315
|
-
is_rate_limit
|
|
316
|
-
self._mark_error(client, is_rate_limit=is_rate_limit)
|
|
322
|
+
self._mark_error(client, is_rate_limit=(e.code == 429))
|
|
317
323
|
last_exc = e
|
|
318
324
|
continue
|
|
319
325
|
except (TransportError, ProviderError) as e:
|
|
320
326
|
self._mark_error(client, is_rate_limit=False)
|
|
321
327
|
last_exc = e
|
|
322
328
|
continue
|
|
329
|
+
else:
|
|
330
|
+
self._mark_success(client)
|
|
331
|
+
return result
|
|
323
332
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
raise last_exc
|
|
333
|
+
if last_exc is None:
|
|
334
|
+
raise BalancerError(
|
|
335
|
+
"http balancer has no available clients (all in cooldown or not connected)"
|
|
336
|
+
)
|
|
329
337
|
|
|
330
|
-
raise
|
|
338
|
+
raise BalancerError(
|
|
339
|
+
f"http failover exhausted after {attempts} attempt(s)"
|
|
340
|
+
) from last_exc
|
|
331
341
|
|
|
332
342
|
try:
|
|
333
343
|
return await asyncio.wait_for(_run(), timeout=self._request_timeout)
|
|
334
344
|
except asyncio.TimeoutError as exc:
|
|
335
345
|
raise ProviderTimeoutError(
|
|
336
346
|
timeout=self._request_timeout,
|
|
337
|
-
endpoint=
|
|
338
|
-
operation="
|
|
347
|
+
endpoint=self.__class__.__name__,
|
|
348
|
+
operation="request",
|
|
339
349
|
) from exc
|
|
340
350
|
|
|
341
|
-
async def
|
|
351
|
+
async def _send_message(self, boc: str) -> None:
|
|
342
352
|
async def _call(client: BaseClient) -> None:
|
|
343
|
-
return await client.
|
|
353
|
+
return await client._send_message(boc)
|
|
344
354
|
|
|
345
|
-
|
|
355
|
+
method = "send_message"
|
|
356
|
+
return await self._with_failover(_call, method)
|
|
346
357
|
|
|
347
|
-
async def
|
|
358
|
+
async def _get_config(self) -> t.Dict[int, t.Any]:
|
|
348
359
|
async def _call(client: BaseClient) -> t.Dict[int, t.Any]:
|
|
349
|
-
return await client.
|
|
360
|
+
return await client._get_config()
|
|
350
361
|
|
|
351
|
-
|
|
362
|
+
method = "get_config"
|
|
363
|
+
return await self._with_failover(_call, method)
|
|
352
364
|
|
|
353
|
-
async def
|
|
354
|
-
async def _call(client: BaseClient) ->
|
|
355
|
-
return await client.
|
|
365
|
+
async def _get_info(self, address: str) -> ContractInfo:
|
|
366
|
+
async def _call(client: BaseClient) -> ContractInfo:
|
|
367
|
+
return await client._get_info(address)
|
|
356
368
|
|
|
357
|
-
|
|
369
|
+
method = "get_info"
|
|
370
|
+
return await self._with_failover(_call, method)
|
|
358
371
|
|
|
359
372
|
async def _get_transactions(
|
|
360
373
|
self,
|
|
@@ -371,7 +384,8 @@ class HttpBalancer(BaseClient):
|
|
|
371
384
|
to_lt=to_lt,
|
|
372
385
|
)
|
|
373
386
|
|
|
374
|
-
|
|
387
|
+
method = "get_transactions"
|
|
388
|
+
return await self._with_failover(_call, method)
|
|
375
389
|
|
|
376
390
|
async def _run_get_method(
|
|
377
391
|
self,
|
|
@@ -386,16 +400,5 @@ class HttpBalancer(BaseClient):
|
|
|
386
400
|
stack=stack,
|
|
387
401
|
)
|
|
388
402
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
async def connect(self) -> None:
|
|
392
|
-
await asyncio.gather(
|
|
393
|
-
*(state.client.connect() for state in self._states),
|
|
394
|
-
return_exceptions=True,
|
|
395
|
-
)
|
|
396
|
-
|
|
397
|
-
async def close(self) -> None:
|
|
398
|
-
await asyncio.gather(
|
|
399
|
-
*(state.client.close() for state in self._states),
|
|
400
|
-
return_exceptions=True,
|
|
401
|
-
)
|
|
403
|
+
method = "run_get_method"
|
|
404
|
+
return await self._with_failover(_call, method)
|
|
@@ -13,7 +13,7 @@ from tonutils.exceptions import ClientError, RunGetMethodError
|
|
|
13
13
|
from tonutils.types import (
|
|
14
14
|
ClientType,
|
|
15
15
|
ContractState,
|
|
16
|
-
|
|
16
|
+
ContractInfo,
|
|
17
17
|
NetworkGlobalID,
|
|
18
18
|
RetryPolicy,
|
|
19
19
|
)
|
|
@@ -72,31 +72,25 @@ class TonapiClient(BaseClient):
|
|
|
72
72
|
)
|
|
73
73
|
|
|
74
74
|
@property
|
|
75
|
-
def
|
|
76
|
-
return self._provider
|
|
77
|
-
|
|
78
|
-
@property
|
|
79
|
-
def is_connected(self) -> bool:
|
|
75
|
+
def connected(self) -> bool:
|
|
80
76
|
session = self._provider.session
|
|
81
77
|
return session is not None and not session.closed
|
|
82
78
|
|
|
83
|
-
|
|
79
|
+
@property
|
|
80
|
+
def provider(self) -> TonapiHttpProvider:
|
|
81
|
+
return self._provider
|
|
82
|
+
|
|
83
|
+
async def connect(self) -> None:
|
|
84
84
|
await self._provider.connect()
|
|
85
|
-
return self
|
|
86
85
|
|
|
87
|
-
async def
|
|
88
|
-
self,
|
|
89
|
-
exc_type: t.Optional[t.Type[BaseException]],
|
|
90
|
-
exc_value: t.Optional[BaseException],
|
|
91
|
-
traceback: t.Optional[t.Any],
|
|
92
|
-
) -> None:
|
|
86
|
+
async def close(self) -> None:
|
|
93
87
|
await self._provider.close()
|
|
94
88
|
|
|
95
|
-
async def
|
|
89
|
+
async def _send_message(self, boc: str) -> None:
|
|
96
90
|
payload = BlockchainMessagePayload(boc=boc)
|
|
97
91
|
return await self.provider.blockchain_message(payload=payload)
|
|
98
92
|
|
|
99
|
-
async def
|
|
93
|
+
async def _get_config(self) -> t.Dict[int, t.Any]:
|
|
100
94
|
result = await self.provider.blockchain_config()
|
|
101
95
|
|
|
102
96
|
if result.raw is None:
|
|
@@ -106,10 +100,10 @@ class TonapiClient(BaseClient):
|
|
|
106
100
|
config_slice = config_cell.begin_parse()
|
|
107
101
|
return parse_stack_config(config_slice)
|
|
108
102
|
|
|
109
|
-
async def
|
|
103
|
+
async def _get_info(self, address: str) -> ContractInfo:
|
|
110
104
|
result = await self.provider.blockchain_account(address)
|
|
111
105
|
|
|
112
|
-
contract_info =
|
|
106
|
+
contract_info = ContractInfo(
|
|
113
107
|
balance=result.balance,
|
|
114
108
|
state=ContractState(result.status),
|
|
115
109
|
last_transaction_lt=result.last_transaction_lt,
|
|
@@ -165,9 +159,3 @@ class TonapiClient(BaseClient):
|
|
|
165
159
|
)
|
|
166
160
|
|
|
167
161
|
return decode_tonapi_stack(result.stack or [])
|
|
168
|
-
|
|
169
|
-
async def connect(self) -> None:
|
|
170
|
-
await self._provider.connect()
|
|
171
|
-
|
|
172
|
-
async def close(self) -> None:
|
|
173
|
-
await self._provider.close()
|
|
@@ -14,7 +14,7 @@ from tonutils.exceptions import ClientError, RunGetMethodError
|
|
|
14
14
|
from tonutils.types import (
|
|
15
15
|
ClientType,
|
|
16
16
|
ContractState,
|
|
17
|
-
|
|
17
|
+
ContractInfo,
|
|
18
18
|
NetworkGlobalID,
|
|
19
19
|
RetryPolicy,
|
|
20
20
|
)
|
|
@@ -70,56 +70,44 @@ class ToncenterClient(BaseClient):
|
|
|
70
70
|
)
|
|
71
71
|
|
|
72
72
|
@property
|
|
73
|
-
def
|
|
74
|
-
return self._provider
|
|
75
|
-
|
|
76
|
-
@property
|
|
77
|
-
def is_connected(self) -> bool:
|
|
73
|
+
def connected(self) -> bool:
|
|
78
74
|
session = self._provider.session
|
|
79
75
|
return session is not None and not session.closed
|
|
80
76
|
|
|
81
|
-
|
|
77
|
+
@property
|
|
78
|
+
def provider(self) -> ToncenterHttpProvider:
|
|
79
|
+
return self._provider
|
|
80
|
+
|
|
81
|
+
async def connect(self) -> None:
|
|
82
82
|
await self._provider.connect()
|
|
83
|
-
return self
|
|
84
83
|
|
|
85
|
-
async def
|
|
86
|
-
self,
|
|
87
|
-
exc_type: t.Optional[t.Type[BaseException]],
|
|
88
|
-
exc_value: t.Optional[BaseException],
|
|
89
|
-
traceback: t.Optional[t.Any],
|
|
90
|
-
) -> None:
|
|
84
|
+
async def close(self) -> None:
|
|
91
85
|
await self._provider.close()
|
|
92
86
|
|
|
93
|
-
async def
|
|
87
|
+
async def _send_message(self, boc: str) -> None:
|
|
94
88
|
payload = SendBocPayload(boc=boc)
|
|
95
89
|
return await self.provider.send_boc(payload=payload)
|
|
96
90
|
|
|
97
|
-
async def
|
|
91
|
+
async def _get_config(self) -> t.Dict[int, t.Any]:
|
|
98
92
|
request = await self.provider.get_config_all()
|
|
99
93
|
|
|
100
94
|
if request.result is None:
|
|
101
|
-
raise ClientError(
|
|
102
|
-
"Invalid get_config_all response: missing 'result' field."
|
|
103
|
-
)
|
|
95
|
+
raise ClientError("Invalid get_config response: missing `result`.")
|
|
104
96
|
|
|
105
97
|
if request.result.config is None:
|
|
106
|
-
raise ClientError(
|
|
107
|
-
"Invalid config response: missing 'config' section in result."
|
|
108
|
-
)
|
|
98
|
+
raise ClientError("Invalid config response: missing `config` in `result`.")
|
|
109
99
|
|
|
110
100
|
if request.result.config.bytes is None:
|
|
111
|
-
raise ClientError(
|
|
112
|
-
"Invalid config response: missing 'bytes' field in 'config' section."
|
|
113
|
-
)
|
|
101
|
+
raise ClientError("Invalid config response: missing `config.bytes`.")
|
|
114
102
|
|
|
115
103
|
config_cell = Cell.one_from_boc(request.result.config.bytes)
|
|
116
104
|
config_slice = config_cell.begin_parse()
|
|
117
105
|
return parse_stack_config(config_slice)
|
|
118
106
|
|
|
119
|
-
async def
|
|
107
|
+
async def _get_info(self, address: str) -> ContractInfo:
|
|
120
108
|
request = await self.provider.get_address_information(address)
|
|
121
109
|
|
|
122
|
-
contract_info =
|
|
110
|
+
contract_info = ContractInfo(
|
|
123
111
|
balance=int(request.result.balance),
|
|
124
112
|
state=ContractState(request.result.state),
|
|
125
113
|
)
|
|
@@ -241,9 +229,3 @@ class ToncenterClient(BaseClient):
|
|
|
241
229
|
)
|
|
242
230
|
|
|
243
231
|
return decode_toncenter_stack(request.result.stack or [])
|
|
244
|
-
|
|
245
|
-
async def connect(self) -> None:
|
|
246
|
-
await self._provider.connect()
|
|
247
|
-
|
|
248
|
-
async def close(self) -> None:
|
|
249
|
-
await self._provider.close()
|
|
@@ -5,6 +5,7 @@ import json
|
|
|
5
5
|
import typing as t
|
|
6
6
|
|
|
7
7
|
import aiohttp
|
|
8
|
+
from pydantic import BaseModel, ValidationError
|
|
8
9
|
|
|
9
10
|
from tonutils.clients.limiter import RateLimiter
|
|
10
11
|
from tonutils.exceptions import (
|
|
@@ -13,6 +14,8 @@ from tonutils.exceptions import (
|
|
|
13
14
|
ProviderTimeoutError,
|
|
14
15
|
RetryLimitError,
|
|
15
16
|
CDN_CHALLENGE_MARKERS,
|
|
17
|
+
TransportError,
|
|
18
|
+
ProviderError,
|
|
16
19
|
)
|
|
17
20
|
from tonutils.types import RetryPolicy
|
|
18
21
|
|
|
@@ -65,17 +68,77 @@ class HttpProvider:
|
|
|
65
68
|
return self._session
|
|
66
69
|
|
|
67
70
|
@property
|
|
68
|
-
def
|
|
71
|
+
def connected(self) -> bool:
|
|
69
72
|
"""Check whether the provider session is initialized and open."""
|
|
70
73
|
return self._session is not None and not self._session.closed
|
|
71
74
|
|
|
75
|
+
@staticmethod
|
|
76
|
+
def _model(model: t.Type[BaseModel], data: t.Any) -> t.Any:
|
|
77
|
+
try:
|
|
78
|
+
return model.model_validate(data)
|
|
79
|
+
except ValidationError as e:
|
|
80
|
+
raise ProviderError(
|
|
81
|
+
f"invalid response: {model.__name__} validation failed"
|
|
82
|
+
) from e
|
|
83
|
+
|
|
84
|
+
async def send_http_request(
|
|
85
|
+
self,
|
|
86
|
+
method: str,
|
|
87
|
+
path: str,
|
|
88
|
+
*,
|
|
89
|
+
params: t.Any = None,
|
|
90
|
+
json_data: t.Any = None,
|
|
91
|
+
) -> t.Any:
|
|
92
|
+
"""Send an HTTP request with retry handling.
|
|
93
|
+
|
|
94
|
+
On provider error, retries the request according to the retry policy
|
|
95
|
+
matched by error code and message. If no rule matches, or retry attempts
|
|
96
|
+
are exhausted, the error is raised.
|
|
97
|
+
|
|
98
|
+
:param method: HTTP method.
|
|
99
|
+
:param path: Endpoint path relative to base_url.
|
|
100
|
+
:param params: Optional query parameters.
|
|
101
|
+
:param json_data: Optional JSON body.
|
|
102
|
+
:return: Parsed response payload.
|
|
103
|
+
"""
|
|
104
|
+
attempts: t.Dict[int, int] = {}
|
|
105
|
+
|
|
106
|
+
while True:
|
|
107
|
+
try:
|
|
108
|
+
return await self._send_once(
|
|
109
|
+
method,
|
|
110
|
+
path,
|
|
111
|
+
params=params,
|
|
112
|
+
json_data=json_data,
|
|
113
|
+
)
|
|
114
|
+
except ProviderResponseError as e:
|
|
115
|
+
policy = self._retry_policy
|
|
116
|
+
if policy is None:
|
|
117
|
+
raise
|
|
118
|
+
|
|
119
|
+
rule = policy.rule_for(e.code, e.message)
|
|
120
|
+
if rule is None:
|
|
121
|
+
raise
|
|
122
|
+
|
|
123
|
+
key = id(rule)
|
|
124
|
+
attempts[key] = attempts.get(key, 0) + 1
|
|
125
|
+
|
|
126
|
+
if attempts[key] >= rule.attempts:
|
|
127
|
+
raise RetryLimitError(
|
|
128
|
+
attempts=attempts[key],
|
|
129
|
+
max_attempts=rule.attempts,
|
|
130
|
+
last_error=e,
|
|
131
|
+
) from e
|
|
132
|
+
|
|
133
|
+
await asyncio.sleep(rule.delay(attempts[key] - 1))
|
|
134
|
+
|
|
72
135
|
async def connect(self) -> None:
|
|
73
136
|
"""Initialize HTTP session if not already connected."""
|
|
74
|
-
if self.
|
|
137
|
+
if self.connected:
|
|
75
138
|
return
|
|
76
139
|
|
|
77
140
|
async with self._connect_lock:
|
|
78
|
-
if self.
|
|
141
|
+
if self.connected:
|
|
79
142
|
return
|
|
80
143
|
|
|
81
144
|
self._session = aiohttp.ClientSession(
|
|
@@ -139,8 +202,12 @@ class HttpProvider:
|
|
|
139
202
|
:param json_data: Optional JSON body.
|
|
140
203
|
:return: Parsed response payload.
|
|
141
204
|
"""
|
|
142
|
-
if not self.
|
|
143
|
-
raise NotConnectedError(
|
|
205
|
+
if not self.connected:
|
|
206
|
+
raise NotConnectedError(
|
|
207
|
+
component="HttpProvider",
|
|
208
|
+
endpoint=self._base_url,
|
|
209
|
+
operation=f"{method} {path}",
|
|
210
|
+
)
|
|
144
211
|
|
|
145
212
|
assert self._session is not None
|
|
146
213
|
url = f"{self._base_url}/{path.lstrip('/')}"
|
|
@@ -166,65 +233,13 @@ class HttpProvider:
|
|
|
166
233
|
endpoint=url,
|
|
167
234
|
operation="http request",
|
|
168
235
|
) from exc
|
|
169
|
-
|
|
170
236
|
except aiohttp.ClientError as exc:
|
|
171
|
-
raise
|
|
172
|
-
code=0,
|
|
173
|
-
message=str(exc),
|
|
237
|
+
raise TransportError(
|
|
174
238
|
endpoint=url,
|
|
239
|
+
operation="http request",
|
|
240
|
+
reason=str(exc),
|
|
175
241
|
) from exc
|
|
176
242
|
|
|
177
|
-
async def send_http_request(
|
|
178
|
-
self,
|
|
179
|
-
method: str,
|
|
180
|
-
path: str,
|
|
181
|
-
*,
|
|
182
|
-
params: t.Any = None,
|
|
183
|
-
json_data: t.Any = None,
|
|
184
|
-
) -> t.Any:
|
|
185
|
-
"""Send an HTTP request with retry handling.
|
|
186
|
-
|
|
187
|
-
On provider error, retries the request according to the retry policy
|
|
188
|
-
matched by error code and message. If no rule matches, or retry attempts
|
|
189
|
-
are exhausted, the error is raised.
|
|
190
|
-
|
|
191
|
-
:param method: HTTP method.
|
|
192
|
-
:param path: Endpoint path relative to base_url.
|
|
193
|
-
:param params: Optional query parameters.
|
|
194
|
-
:param json_data: Optional JSON body.
|
|
195
|
-
:return: Parsed response payload.
|
|
196
|
-
"""
|
|
197
|
-
attempts: t.Dict[int, int] = {}
|
|
198
|
-
|
|
199
|
-
while True:
|
|
200
|
-
try:
|
|
201
|
-
return await self._send_once(
|
|
202
|
-
method,
|
|
203
|
-
path,
|
|
204
|
-
params=params,
|
|
205
|
-
json_data=json_data,
|
|
206
|
-
)
|
|
207
|
-
except ProviderResponseError as e:
|
|
208
|
-
policy = self._retry_policy
|
|
209
|
-
if policy is None:
|
|
210
|
-
raise
|
|
211
|
-
|
|
212
|
-
rule = policy.rule_for(e.code, e.message)
|
|
213
|
-
if rule is None:
|
|
214
|
-
raise
|
|
215
|
-
|
|
216
|
-
key = id(rule)
|
|
217
|
-
attempts[key] = attempts.get(key, 0) + 1
|
|
218
|
-
|
|
219
|
-
if attempts[key] >= rule.attempts:
|
|
220
|
-
raise RetryLimitError(
|
|
221
|
-
attempts=attempts[key],
|
|
222
|
-
max_attempts=rule.attempts,
|
|
223
|
-
last_error=e,
|
|
224
|
-
) from e
|
|
225
|
-
|
|
226
|
-
await asyncio.sleep(rule.delay(attempts[key] - 1))
|
|
227
|
-
|
|
228
243
|
@classmethod
|
|
229
244
|
def _detect_proxy_error(
|
|
230
245
|
cls,
|
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
import typing as t
|
|
4
4
|
|
|
5
5
|
import aiohttp
|
|
6
|
-
from pydantic import BaseModel
|
|
7
6
|
|
|
8
7
|
from tonutils.clients.http.provider.base import HttpProvider
|
|
9
8
|
from tonutils.clients.http.provider.models import (
|
|
@@ -50,10 +49,6 @@ class TonapiHttpProvider(HttpProvider):
|
|
|
50
49
|
retry_policy=retry_policy,
|
|
51
50
|
)
|
|
52
51
|
|
|
53
|
-
@staticmethod
|
|
54
|
-
def _model(model: t.Type[BaseModel], data: t.Any) -> t.Any:
|
|
55
|
-
return model.model_validate(data)
|
|
56
|
-
|
|
57
52
|
async def blockchain_message(
|
|
58
53
|
self,
|
|
59
54
|
payload: BlockchainMessagePayload,
|