tonutils 2.0.1b2__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 (76) 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 +4 -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/vanity.py +1 -1
  43. tonutils/contracts/wallet/__init__.py +2 -0
  44. tonutils/contracts/wallet/base.py +3 -3
  45. tonutils/contracts/wallet/messages.py +1 -1
  46. tonutils/contracts/wallet/methods.py +2 -2
  47. tonutils/{protocols/wallet.py → contracts/wallet/protocol.py} +35 -35
  48. tonutils/contracts/wallet/versions/v5.py +3 -3
  49. tonutils/exceptions.py +134 -226
  50. tonutils/types.py +115 -0
  51. tonutils/utils.py +3 -3
  52. {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b3.dist-info}/METADATA +2 -2
  53. tonutils-2.0.1b3.dist-info/RECORD +93 -0
  54. tonutils/clients/adnl/provider/limiter.py +0 -56
  55. tonutils/clients/adnl/stack.py +0 -64
  56. tonutils/clients/http/chainstack/__init__.py +0 -4
  57. tonutils/clients/http/chainstack/client.py +0 -63
  58. tonutils/clients/http/chainstack/provider.py +0 -44
  59. tonutils/clients/http/quicknode/__init__.py +0 -4
  60. tonutils/clients/http/quicknode/client.py +0 -60
  61. tonutils/clients/http/quicknode/provider.py +0 -42
  62. tonutils/clients/http/tatum/__init__.py +0 -4
  63. tonutils/clients/http/tatum/client.py +0 -66
  64. tonutils/clients/http/tatum/provider.py +0 -53
  65. tonutils/clients/http/tonapi/__init__.py +0 -4
  66. tonutils/clients/http/tonapi/provider.py +0 -150
  67. tonutils/clients/http/tonapi/stack.py +0 -71
  68. tonutils/clients/http/toncenter/__init__.py +0 -4
  69. tonutils/clients/http/toncenter/provider.py +0 -145
  70. tonutils/clients/http/toncenter/stack.py +0 -73
  71. tonutils/protocols/__init__.py +0 -9
  72. tonutils-2.0.1b2.dist-info/RECORD +0 -98
  73. /tonutils/{protocols/client.py → clients/protocol.py} +0 -0
  74. {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b3.dist-info}/WHEEL +0 -0
  75. {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b3.dist-info}/licenses/LICENSE +0 -0
  76. {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b3.dist-info}/top_level.txt +0 -0
tonutils/exceptions.py CHANGED
@@ -1,294 +1,202 @@
1
+ import asyncio
1
2
  import typing as t
2
3
 
3
- from pytoniq_core import Address
4
-
5
4
  __all__ = [
6
- "TonutilsException",
7
- "ClientNotConnectedError",
8
- "NotRefreshedError",
9
- "ContractError",
5
+ "TonutilsError",
6
+ "TransportError",
7
+ "ProviderError",
10
8
  "ClientError",
11
- "AdnlServerError",
12
- "AdnlProviderError",
13
- "AdnlProviderConnectError",
14
- "AdnlProviderClosedError",
15
- "AdnlProviderResponseError",
16
- "AdnlProviderMissingBlockError",
17
- "AdnlBalancerConnectionError",
18
- "RateLimitExceededError",
9
+ "ContractError",
10
+ "BalancerError",
11
+ "NotConnectedError",
12
+ "ProviderTimeoutError",
13
+ "ProviderResponseError",
14
+ "RetryLimitError",
19
15
  "RunGetMethodError",
20
- "AdnlHandshakeError",
21
- "AdnlTransportError",
22
- "AdnlTransportStateError",
23
- "AdnlTransportCipherError",
24
- "AdnlTransportFrameError",
16
+ "StateNotLoadedError",
17
+ "CDN_CHALLENGE_MARKERS",
25
18
  ]
26
19
 
27
20
 
28
- class TonutilsException(Exception):
29
- """Base exception for all tonutils-specific errors."""
21
+ class TonutilsError(Exception):
22
+ """Base exception for tonutils."""
30
23
 
31
- @classmethod
32
- def _obj_name(cls, obj: t.Union[object, type, str]) -> str:
33
- """
34
- Resolve a human-readable class name from an object or type.
35
24
 
36
- :param obj: Instance, class or string name
37
- :return: Resolved name used in error messages
38
- """
39
- if isinstance(obj, type):
40
- return obj.__name__
41
- if isinstance(obj, str):
42
- return obj
43
- return obj.__class__.__name__
25
+ class TransportError(TonutilsError):
26
+ """Raise on transport-level failures (I/O, handshake, crypto, socket)."""
44
27
 
45
28
 
46
- class ClientNotConnectedError(TonutilsException):
47
- """
48
- Raised when a client method is called without an active connection.
29
+ class ProviderError(TonutilsError):
30
+ """Raise on provider-level failures (protocol, parsing, session/state)."""
49
31
 
50
- This usually indicates that connect() or an async context manager
51
- was not used before making network requests.
52
- """
53
32
 
54
- def __init__(self, obj: t.Union[object, type, str]) -> None:
55
- name = self._obj_name(obj)
56
- super().__init__(
57
- f"`{name}` is not connected.\n"
58
- f"Use `async with {name}(...) as client:` "
59
- f"or call `await {name}(...).connect()` before making requests."
60
- )
33
+ class ClientError(TonutilsError):
34
+ """Raise on client misuse, validation errors, or unsupported operations."""
61
35
 
62
36
 
63
- class NotRefreshedError(TonutilsException):
64
- """
65
- Raised when accessing derived state before an explicit refresh.
66
-
67
- Typical usage is for contract wrappers that require refresh()
68
- to be called before accessing state_info or derived properties.
69
- """
37
+ class BalancerError(TonutilsError):
38
+ """Raise on balancer failures (no alive backends, failover exhausted)."""
70
39
 
71
- def __init__(self, obj: t.Union[object, type, str], attr: str) -> None:
72
- name = self._obj_name(obj)
73
- super().__init__(
74
- f"Access to `{attr}` is not allowed.\n"
75
- f"Call `await {name}.refresh()` before accessing `{attr}`."
76
- )
77
40
 
41
+ class NotConnectedError(TonutilsError, RuntimeError):
42
+ """Raise when an operation requires an active connection.
78
43
 
79
- class ContractError(TonutilsException):
44
+ Typically means the underlying client/provider is not connected yet or was closed.
80
45
  """
81
- Generic error related to smart contract helpers.
82
46
 
83
- Used for configuration issues, invalid versions and similar
84
- contract wrapper problems.
85
- """
47
+ def __init__(self) -> None:
48
+ super().__init__("not connected. Use `await connect()` or `async with ...`.")
86
49
 
87
- def __init__(self, obj: t.Union[object, type, str], message: str) -> None:
88
- super().__init__(f"{self._obj_name(obj)}: {message}.")
89
50
 
51
+ class ProviderTimeoutError(ProviderError, asyncio.TimeoutError):
52
+ """Raise when a provider operation exceeds its timeout.
90
53
 
91
- class ClientError(TonutilsException):
92
- """
93
- Base error for client-side failures.
54
+ Used for both ADNL and HTTP providers.
94
55
 
95
- Used for issues related to specific client implementations
96
- (HTTP, ADNL, balancers, etc.).
56
+ :param timeout: Timeout in seconds.
57
+ :param endpoint: Endpoint identifier (URL or host:port).
58
+ :param operation: Operation label (e.g. "adnl query", "http request").
97
59
  """
98
60
 
61
+ timeout: float
62
+ endpoint: str
63
+ operation: str
99
64
 
100
- class AdnlServerError(ClientError):
101
- """
102
- Lite-server reported an internal error while processing a request.
103
-
104
- Wraps lite-server error code and message as returned by ADNL.
105
- """
65
+ def __init__(self, *, timeout: float, endpoint: str, operation: str) -> None:
66
+ self.timeout = float(timeout)
67
+ self.endpoint = endpoint
68
+ self.operation = operation
69
+ super().__init__(f"{operation} timed out after {timeout}s: {endpoint}")
106
70
 
107
- def __init__(self, code: int, message: str) -> None:
108
- """
109
- :param code: Lite-server error code
110
- :param message: Lite-server error message
111
- """
112
- super().__init__(f"Lite-server crashed with `{code}` code. Message: {message}.")
113
- self.message = message
114
- self.code = code
115
71
 
72
+ class ProviderResponseError(ProviderError):
73
+ """Raise when a backend returns an error response.
116
74
 
117
- class AdnlProviderError(ClientError):
118
- """
119
- Base error for ADNL provider failures.
75
+ This is a normalized provider error for:
76
+ - HTTP status codes (e.g. 429/5xx)
77
+ - lite-server numeric error codes
120
78
 
121
- Includes additional context about the lite-server host and port.
79
+ :param code: Backend code (HTTP status or lite-server code).
80
+ :param message: Backend error description.
81
+ :param endpoint: Endpoint identifier (URL or host:port).
122
82
  """
123
83
 
124
- def __init__(self, message: str, host: str, port: int) -> None:
125
- """
126
- :param message: Error description
127
- :param host: Lite-server host
128
- :param port: Lite-server port
129
- """
130
- full_message = f"{message} ({host}:{port})."
131
- super().__init__(full_message)
132
- self.host = host
133
- self.port = port
134
-
84
+ code: int
85
+ message: str
86
+ endpoint: str
135
87
 
136
- class AdnlProviderConnectError(AdnlProviderError):
137
- """
138
- Failed to establish an ADNL connection to the lite-server.
139
-
140
- Wraps network or handshake errors that occur during connect().
141
- """
88
+ def __init__(self, *, code: int, message: str, endpoint: str) -> None:
89
+ self.code = int(code)
90
+ self.message = message
91
+ self.endpoint = endpoint
92
+ super().__init__(f"request failed with code {code} at {endpoint}: {message}")
142
93
 
143
- def __init__(self, host: str, port: int, message: str) -> None:
144
- super().__init__(
145
- f"Failed to connect: {message}.",
146
- host=host,
147
- port=port,
148
- )
149
94
 
95
+ class RetryLimitError(ProviderError):
96
+ """Raise when retry policy is exhausted for a matched rule.
150
97
 
151
- class AdnlProviderClosedError(AdnlProviderError):
98
+ :param attempts: Attempts already performed for the matched rule.
99
+ :param max_attempts: Maximum attempts allowed by the matched rule.
100
+ :param last_error: Last provider error that triggered a retry.
152
101
  """
153
- ADNL provider was closed while waiting for a response.
154
102
 
155
- Typically raised when transport is torn down during an in-flight
156
- request or when the remote peer closes connection unexpectedly.
157
- """
103
+ attempts: int
104
+ max_attempts: int
105
+ last_error: ProviderError
158
106
 
159
- def __init__(self, host: str, port: int) -> None:
107
+ def __init__(
108
+ self,
109
+ *,
110
+ attempts: int,
111
+ max_attempts: int,
112
+ last_error: ProviderError,
113
+ ) -> None:
114
+ self.attempts = int(attempts)
115
+ self.max_attempts = int(max_attempts)
116
+ self.last_error = last_error
160
117
  super().__init__(
161
- "Provider closed while waiting response.",
162
- host=host,
163
- port=port,
118
+ f"retry exhausted ({self.attempts}/{self.max_attempts}). "
119
+ f"Last error: {last_error}"
164
120
  )
165
121
 
166
122
 
167
- class AdnlProviderResponseError(AdnlProviderError):
168
- """
169
- Received an invalid or malformed response from lite-server.
123
+ class ContractError(ClientError):
124
+ """Raise when a contract wrapper operation fails.
170
125
 
171
- Raised when the ADNL response payload does not match the expected
172
- structure or type.
126
+ :param target: Contract instance or contract class related to the failure.
127
+ :param message: Human-readable error message.
173
128
  """
174
129
 
175
- def __init__(self, host: str, port: int) -> None:
176
- super().__init__(
177
- "Invalid response from provider.",
178
- host=host,
179
- port=port,
180
- )
130
+ target: t.Any
131
+ message: str
181
132
 
133
+ def __init__(self, target: t.Any, message: str) -> None:
134
+ self.target = target
135
+ self.message = message
182
136
 
183
- class AdnlProviderMissingBlockError(AdnlProviderError):
184
- """
185
- Lite-server reported that a requested block is missing.
186
-
187
- Used for specific lite-server error codes that indicate
188
- absence of the requested block.
189
- """
190
-
191
- def __init__(self, attempts: int, host: str, port: int, message: str) -> None:
192
- super().__init__(
193
- f"Cannot load block after {attempts} attempts: {message}.",
194
- host=host,
195
- port=port,
137
+ name = (
138
+ target.__name__ if isinstance(target, type) else target.__class__.__name__
196
139
  )
197
- self.attempts = attempts
140
+ super().__init__(f"{name}: {message}")
198
141
 
199
142
 
200
- class AdnlBalancerConnectionError(ClientError):
201
- """
202
- All ADNL lite-server providers failed to connect or process a request.
203
-
204
- Raised by AdnlBalancer when no healthy providers remain.
205
- """
143
+ class StateNotLoadedError(ContractError):
144
+ """Raise when a contract wrapper requires state that is not loaded.
206
145
 
146
+ Typical cases:
147
+ - state_info not fetched
148
+ - state_data not decoded/available
207
149
 
208
- class RateLimitExceededError(ClientError):
150
+ :param contract: Contract instance related to the failure.
151
+ :param missing: Missing field name (e.g. "state_info", "state_data").
209
152
  """
210
- Request was retried multiple times but rate limits could not be bypassed.
211
153
 
212
- Raised after exhausting configured retry attempts.
213
- """
154
+ missing: str
214
155
 
215
- def __init__(self, attempts: int) -> None:
216
- """
217
- :param attempts: Number of attempts performed before giving up
218
- """
219
- super().__init__(f"Rate limit exceeded after `{attempts}` attempts.")
220
- self.attempts = attempts
156
+ def __init__(self, contract: t.Any, *, missing: str) -> None:
157
+ self.missing = missing
158
+ name = contract.__class__.__name__
159
+ super().__init__(contract, f"{missing} is not loaded. Call {name}.refresh().")
221
160
 
222
161
 
223
162
  class RunGetMethodError(ClientError):
224
- """
225
- get-method execution failed with a non-zero exit code.
163
+ """Raise when a contract get-method returns a non-zero TVM exit code.
226
164
 
227
- Raised when lite-server returns exit_code != 0 for a runSmcMethod call.
165
+ :param address: Contract address (string form).
166
+ :param method_name: Get-method name.
167
+ :param exit_code: TVM exit code.
228
168
  """
229
169
 
230
- def __init__(self, address: Address, method_name: str, exit_code: int) -> None:
231
- """
232
- :param address: Contract address on which get-method was executed
233
- :param method_name: Name of the get-method
234
- :param exit_code: Non-zero TVM exit code returned by the method
235
- """
170
+ address: str
171
+ method_name: str
172
+ exit_code: int
173
+
174
+ def __init__(self, *, address: str, method_name: str, exit_code: int) -> None:
175
+ self.address = address
176
+ self.method_name = method_name
177
+ self.exit_code = int(exit_code)
236
178
  super().__init__(
237
- f"Get method `{method_name}` on `{address.to_str()}` "
238
- f"failed with `{exit_code}` exit code."
179
+ f"get-method `{method_name}` failed for {address} (exit code {self.exit_code})."
239
180
  )
240
- self.method_name = method_name
241
- self.exit_code = exit_code
242
- self.address = address
243
-
244
-
245
- class AdnlTransportError(TonutilsException):
246
- """
247
- Base error for raw ADNL transport failures.
248
-
249
- Covers handshake, cipher initialization, framing and state issues.
250
- """
251
-
252
-
253
- class AdnlHandshakeError(AdnlTransportError):
254
- """
255
- ADNL handshake failed during initial connection.
256
181
 
257
- Raised when the remote side closes the connection or does not
258
- respond within the expected timeout.
259
- """
260
-
261
-
262
- class AdnlTransportStateError(AdnlTransportError):
263
- """
264
- Invalid internal state of the ADNL transport.
265
-
266
- Raised when required transport components (reader, writer, cipher, etc.)
267
- are not initialized or used incorrectly.
268
- """
269
-
270
- def __init__(self, message: str) -> None:
271
- super().__init__(f"ADNL transport state error: {message}.")
272
-
273
-
274
- class AdnlTransportCipherError(AdnlTransportError):
275
- """
276
- ADNL cipher was used before being initialized.
277
-
278
- Raised when trying to encrypt or decrypt frames without
279
- a valid session cipher.
280
- """
281
-
282
- def __init__(self, direction: str) -> None:
283
- super().__init__(f"ADNL {direction} cipher is not initialized.")
284
-
285
-
286
- class AdnlTransportFrameError(AdnlTransportError):
287
- """
288
- Malformed or invalid ADNL frame was received.
289
-
290
- Raised when frame length, structure or checksum validation fails.
291
- """
292
182
 
293
- def __init__(self, reason: str) -> None:
294
- super().__init__(f"Invalid ADNL frame: {reason}.")
183
+ CDN_CHALLENGE_MARKERS: t.Dict[str, str] = {
184
+ # Cloudflare
185
+ "cloudflare": "Cloudflare protection triggered or blocked the request.",
186
+ "cf-ray": "Cloudflare intermediate error (cf-ray header detected).",
187
+ "just a moment": "Cloudflare browser verification page.",
188
+ "checking your browser": "Cloudflare browser verification page.",
189
+ "attention required": "Cloudflare challenge page.",
190
+ "captcha": "Cloudflare CAPTCHA challenge.",
191
+ # Other CDNs / proxies
192
+ "akamai": "Akamai CDN blocked or intercepted the request.",
193
+ "fastly": "Fastly CDN error response detected.",
194
+ "varnish": "Varnish cache/CDN interference.",
195
+ "nginx": "Reverse proxy (nginx) error response.",
196
+ # Upstream failures
197
+ "502 bad gateway": "Bad gateway from upstream or proxy.",
198
+ "503 service unavailable": "Service temporarily unavailable (proxy or CDN).",
199
+ "ddos": "Possible DDoS protection or mitigation page.",
200
+ }
201
+ """Markers for detecting CDN / proxy challenge and anti-DDoS responses,
202
+ used for error normalization and default retry policies."""
tonutils/types.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import base64
4
4
  import typing as t
5
+ from dataclasses import dataclass
5
6
  from enum import Enum
6
7
 
7
8
  from nacl.signing import SigningKey
@@ -16,6 +17,8 @@ __all__ = [
16
17
  "ClientType",
17
18
  "ContractState",
18
19
  "ContractStateInfo",
20
+ "DEFAULT_ADNL_RETRY_POLICY",
21
+ "DEFAULT_HTTP_RETRY_POLICY",
19
22
  "DEFAULT_SENDMODE",
20
23
  "DEFAULT_SUBWALLET_ID",
21
24
  "DNSCategory",
@@ -24,6 +27,8 @@ __all__ = [
24
27
  "NetworkGlobalID",
25
28
  "PrivateKey",
26
29
  "PublicKey",
30
+ "RetryPolicy",
31
+ "RetryRule",
27
32
  "SendMode",
28
33
  "StackItem",
29
34
  "StackItems",
@@ -31,6 +36,8 @@ __all__ = [
31
36
  "WorkchainID",
32
37
  ]
33
38
 
39
+ from tonutils.exceptions import CDN_CHALLENGE_MARKERS
40
+
34
41
  AddressLike = t.Union[Address, str]
35
42
  """Type alias for TON address inputs. Accepts either an Address object or string representation."""
36
43
 
@@ -375,6 +382,114 @@ class BagID(ADNL):
375
382
  """TON Storage bag identifier (32 bytes)."""
376
383
 
377
384
 
385
+ @dataclass(slots=True, frozen=True)
386
+ class RetryRule:
387
+ """
388
+ Retry rule matched by numeric code and/or message substrings.
389
+
390
+ Matching:
391
+ - if codes is set: code must be in codes
392
+ - if markers is set: any marker must be present in message (case-insensitive)
393
+ - if both are set: both conditions must match
394
+
395
+ Attributes:
396
+ attempts: Maximum number of retry attempts
397
+ base_delay: Initial delay before first retry (seconds)
398
+ cap_delay: Maximum delay between retries (seconds)
399
+ codes: Error or status codes this rule applies to
400
+ markers: Case-insensitive substrings matched against error message
401
+ """
402
+
403
+ attempts: int = 3
404
+ base_delay: float = 0.3
405
+ cap_delay: float = 3.0
406
+
407
+ codes: t.Optional[t.Tuple[int, ...]] = None
408
+ markers: t.Optional[t.Tuple[str, ...]] = None
409
+
410
+ def __post_init__(self) -> None:
411
+ if self.attempts < 1:
412
+ raise ValueError("attempts must be >= 1")
413
+ if self.base_delay < 0:
414
+ raise ValueError("base_delay must be >= 0")
415
+ if self.cap_delay < 0:
416
+ raise ValueError("cap_delay must be >= 0")
417
+ if self.cap_delay < self.base_delay:
418
+ raise ValueError("cap_delay must be >= base_delay")
419
+ if self.markers:
420
+ norm = tuple(m.strip().lower() for m in self.markers if m and m.strip())
421
+ object.__setattr__(self, "markers", norm or None)
422
+
423
+ def matches(self, code: int, message: t.Any) -> bool:
424
+ if self.codes is not None and code not in self.codes:
425
+ return False
426
+
427
+ if self.markers:
428
+ msg = str(message or "").lower()
429
+ if not any(m in msg for m in self.markers):
430
+ return False
431
+
432
+ return True
433
+
434
+ def delay(self, attempt_index: int) -> float:
435
+ if attempt_index < 0:
436
+ raise ValueError("attempt_index must be >= 0")
437
+ d = self.base_delay * (2**attempt_index)
438
+ return d if d < self.cap_delay else self.cap_delay
439
+
440
+
441
+ @dataclass(slots=True, frozen=True)
442
+ class RetryPolicy:
443
+ """Ordered collection of retry rules (first match wins)."""
444
+
445
+ rules: t.Tuple[RetryRule, ...]
446
+
447
+ def rule_for(self, code: int, message: t.Any) -> t.Optional[RetryRule]:
448
+ for r in self.rules:
449
+ if r.matches(code, message):
450
+ return r
451
+ return None
452
+
453
+
454
+ DEFAULT_HTTP_RETRY_POLICY = RetryPolicy(
455
+ rules=(
456
+ # rate limit exceed
457
+ RetryRule(
458
+ codes=(429,),
459
+ attempts=3,
460
+ base_delay=0.3,
461
+ cap_delay=3.0,
462
+ ),
463
+ # transient gateway/service failures
464
+ RetryRule(
465
+ codes=(502, 503, 504),
466
+ attempts=3,
467
+ base_delay=0.5,
468
+ cap_delay=5.0,
469
+ ),
470
+ # CDN/protection/challenge pages (сloudflare, etc.)
471
+ RetryRule(
472
+ attempts=3,
473
+ base_delay=1.0,
474
+ cap_delay=8.0,
475
+ markers=tuple(CDN_CHALLENGE_MARKERS.keys()),
476
+ ),
477
+ )
478
+ )
479
+ """Default retry policy for HTTP queries."""
480
+
481
+ DEFAULT_ADNL_RETRY_POLICY = RetryPolicy(
482
+ rules=(
483
+ # rate limit exceed
484
+ RetryRule(codes=(228, 5556), attempts=3),
485
+ # block (...) is not in db
486
+ RetryRule(codes=(651,), attempts=4),
487
+ # backend node timeout
488
+ RetryRule(codes=(502,), attempts=5),
489
+ )
490
+ )
491
+ """Default retry policy for ADNL queries."""
492
+
378
493
  DEFAULT_SUBWALLET_ID = 698983191
379
494
  """Default subwallet ID for wallet contracts."""
380
495
 
tonutils/utils.py CHANGED
@@ -34,7 +34,7 @@ from tonutils.types import (
34
34
  )
35
35
 
36
36
  if t.TYPE_CHECKING:
37
- from tonutils.protocols import ClientProtocol
37
+ from tonutils.clients.protocol import ClientProtocol
38
38
 
39
39
 
40
40
  __all__ = [
@@ -229,7 +229,7 @@ def norm_stack_cell(
229
229
  return maybe_stack_addr(cell)
230
230
 
231
231
 
232
- def parse_stack_config(config_slice: Slice) -> dict[int, t.Any]:
232
+ def parse_stack_config(config_slice: Slice) -> t.Dict[int, t.Any]:
233
233
  """
234
234
  Parse blockchain configuration parameters from a config cell.
235
235
 
@@ -514,7 +514,7 @@ class TextCipher:
514
514
  """
515
515
  Decrypt an encrypted text message.
516
516
 
517
- Decrypts a message that was encrypted with the encrypt() method.
517
+ Decrypts a message that was encrypted with the encrypt() method
518
518
  Verifies message integrity using HMAC authentication.
519
519
 
520
520
  :param payload: Encrypted message as Cell, hex string, base64 string, or bytes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tonutils
3
- Version: 2.0.1b2
3
+ Version: 2.0.1b3
4
4
  Summary: Tonutils is a high-level, object-oriented Python library designed to facilitate seamless interactions with the TON blockchain.
5
5
  Author: nessshon
6
6
  Maintainer: nessshon
@@ -26,11 +26,11 @@ Requires-Python: <3.15,>=3.10
26
26
  Description-Content-Type: text/markdown
27
27
  License-File: LICENSE
28
28
  Requires-Dist: aiohttp>=3.7.0
29
- Requires-Dist: pyapiq>=0.2.1
30
29
  Requires-Dist: pycryptodomex~=3.23.0
31
30
  Requires-Dist: pydantic<3.0,>=2.0
32
31
  Requires-Dist: pynacl~=1.6.0
33
32
  Requires-Dist: pytoniq-core~=0.1.45
33
+ Requires-Dist: requests>=2.31.0
34
34
  Dynamic: license-file
35
35
 
36
36
  # 📦 Tonutils 2.0 [BETA]