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.
- tonutils/__init__.py +0 -2
- tonutils/__meta__.py +1 -1
- tonutils/clients/__init__.py +5 -9
- tonutils/clients/adnl/__init__.py +5 -1
- tonutils/clients/adnl/balancer.py +319 -125
- tonutils/clients/adnl/client.py +187 -51
- tonutils/clients/adnl/provider/config.py +19 -25
- tonutils/clients/adnl/provider/models.py +4 -0
- tonutils/clients/adnl/provider/provider.py +191 -145
- tonutils/clients/adnl/provider/transport.py +38 -32
- 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 +134 -226
- tonutils/types.py +115 -0
- tonutils/utils.py +3 -3
- {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b3.dist-info}/METADATA +2 -2
- tonutils-2.0.1b3.dist-info/RECORD +93 -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.1b3.dist-info}/WHEEL +0 -0
- {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b3.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"ContractError",
|
|
5
|
+
"TonutilsError",
|
|
6
|
+
"TransportError",
|
|
7
|
+
"ProviderError",
|
|
10
8
|
"ClientError",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"AdnlBalancerConnectionError",
|
|
18
|
-
"RateLimitExceededError",
|
|
9
|
+
"ContractError",
|
|
10
|
+
"BalancerError",
|
|
11
|
+
"NotConnectedError",
|
|
12
|
+
"ProviderTimeoutError",
|
|
13
|
+
"ProviderResponseError",
|
|
14
|
+
"RetryLimitError",
|
|
19
15
|
"RunGetMethodError",
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"AdnlTransportStateError",
|
|
23
|
-
"AdnlTransportCipherError",
|
|
24
|
-
"AdnlTransportFrameError",
|
|
16
|
+
"StateNotLoadedError",
|
|
17
|
+
"CDN_CHALLENGE_MARKERS",
|
|
25
18
|
]
|
|
26
19
|
|
|
27
20
|
|
|
28
|
-
class
|
|
29
|
-
"""Base exception for
|
|
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
|
-
|
|
37
|
-
|
|
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
|
|
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
|
-
|
|
55
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
92
|
-
"""
|
|
93
|
-
Base error for client-side failures.
|
|
54
|
+
Used for both ADNL and HTTP providers.
|
|
94
55
|
|
|
95
|
-
|
|
96
|
-
(
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
103
|
+
attempts: int
|
|
104
|
+
max_attempts: int
|
|
105
|
+
last_error: ProviderError
|
|
158
106
|
|
|
159
|
-
def __init__(
|
|
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
|
-
"
|
|
162
|
-
|
|
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
|
|
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
|
-
|
|
172
|
-
|
|
126
|
+
:param target: Contract instance or contract class related to the failure.
|
|
127
|
+
:param message: Human-readable error message.
|
|
173
128
|
"""
|
|
174
129
|
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
+
super().__init__(f"{name}: {message}")
|
|
198
141
|
|
|
199
142
|
|
|
200
|
-
class
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
"""
|
|
154
|
+
missing: str
|
|
214
155
|
|
|
215
|
-
def __init__(self,
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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"
|
|
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
|
-
|
|
294
|
-
|
|
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.
|
|
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) ->
|
|
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.
|
|
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]
|