tonutils 2.0.1b3__py3-none-any.whl → 2.0.1b5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tonutils/__meta__.py +1 -1
- tonutils/cli.py +111 -0
- tonutils/clients/__init__.py +4 -4
- tonutils/clients/adnl/__init__.py +4 -4
- tonutils/clients/adnl/balancer.py +58 -58
- tonutils/clients/adnl/client.py +20 -20
- tonutils/clients/adnl/provider/config.py +13 -8
- tonutils/clients/adnl/provider/provider.py +39 -42
- tonutils/clients/adnl/provider/transport.py +30 -25
- tonutils/clients/base.py +5 -1
- tonutils/exceptions.py +41 -31
- tonutils/tonconnect/__init__.py +0 -0
- tonutils/tools/__init__.py +6 -0
- tonutils/tools/block_scanner/__init__.py +16 -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 +313 -0
- tonutils/tools/block_scanner/traversal.py +97 -0
- tonutils/tools/block_scanner/where.py +53 -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 +12 -4
- {tonutils-2.0.1b3.dist-info → tonutils-2.0.1b5.dist-info}/METADATA +2 -5
- {tonutils-2.0.1b3.dist-info → tonutils-2.0.1b5.dist-info}/RECORD +31 -16
- tonutils-2.0.1b5.dist-info/entry_points.txt +2 -0
- {tonutils-2.0.1b3.dist-info → tonutils-2.0.1b5.dist-info}/WHEEL +0 -0
- {tonutils-2.0.1b3.dist-info → tonutils-2.0.1b5.dist-info}/licenses/LICENSE +0 -0
- {tonutils-2.0.1b3.dist-info → tonutils-2.0.1b5.dist-info}/top_level.txt +0 -0
|
@@ -96,7 +96,11 @@ class AdnlProvider:
|
|
|
96
96
|
|
|
97
97
|
@property
|
|
98
98
|
def is_connected(self) -> bool:
|
|
99
|
-
"""
|
|
99
|
+
"""
|
|
100
|
+
Whether the underlying transport is currently connected.
|
|
101
|
+
|
|
102
|
+
:return: True if connected, False otherwise
|
|
103
|
+
"""
|
|
100
104
|
return self.transport.is_connected
|
|
101
105
|
|
|
102
106
|
@property
|
|
@@ -129,9 +133,9 @@ class AdnlProvider:
|
|
|
129
133
|
@property
|
|
130
134
|
def last_ping_ms(self) -> t.Optional[int]:
|
|
131
135
|
"""
|
|
132
|
-
Round-trip time of the last ping
|
|
136
|
+
Round-trip time of the last successful ping.
|
|
133
137
|
|
|
134
|
-
:return: Ping RTT in milliseconds or None if unknown
|
|
138
|
+
:return: Ping RTT in milliseconds, or None if unknown
|
|
135
139
|
"""
|
|
136
140
|
if self.last_ping_rtt is None:
|
|
137
141
|
return None
|
|
@@ -165,10 +169,7 @@ class AdnlProvider:
|
|
|
165
169
|
return
|
|
166
170
|
|
|
167
171
|
self.loop = asyncio.get_running_loop()
|
|
168
|
-
|
|
169
|
-
await self.transport.connect()
|
|
170
|
-
except Exception as exc:
|
|
171
|
-
raise ProviderError(f"adnl connect failed ({self.node.endpoint})") from exc
|
|
172
|
+
await self.transport.connect()
|
|
172
173
|
|
|
173
174
|
tasks = [self.reader.start(), self.pinger.start(), self.updater.start()]
|
|
174
175
|
await asyncio.gather(*tasks, return_exceptions=True)
|
|
@@ -239,19 +240,13 @@ class AdnlProvider:
|
|
|
239
240
|
raise ProviderTimeoutError(
|
|
240
241
|
timeout=self.request_timeout,
|
|
241
242
|
endpoint=self.node.endpoint,
|
|
242
|
-
operation="
|
|
243
|
-
) from exc
|
|
244
|
-
except asyncio.CancelledError as exc:
|
|
245
|
-
raise ProviderError(
|
|
246
|
-
f"adnl provider closed ({self.node.endpoint})"
|
|
243
|
+
operation="request",
|
|
247
244
|
) from exc
|
|
248
245
|
|
|
249
246
|
if not isinstance(resp, dict):
|
|
250
|
-
raise ProviderError(
|
|
251
|
-
f"adnl invalid response type ({self.node.endpoint})"
|
|
252
|
-
)
|
|
253
|
-
|
|
247
|
+
raise ProviderError(f"invalid response type: {type(resp).__name__}")
|
|
254
248
|
return resp
|
|
249
|
+
|
|
255
250
|
finally:
|
|
256
251
|
if query_id_key in self.pending:
|
|
257
252
|
del self.pending[query_id_key]
|
|
@@ -419,7 +414,7 @@ class AdnlProvider:
|
|
|
419
414
|
self,
|
|
420
415
|
workchain: WorkchainID,
|
|
421
416
|
shard: int,
|
|
422
|
-
seqno: int =
|
|
417
|
+
seqno: t.Optional[int] = None,
|
|
423
418
|
lt: t.Optional[int] = None,
|
|
424
419
|
utime: t.Optional[int] = None,
|
|
425
420
|
*,
|
|
@@ -430,42 +425,45 @@ class AdnlProvider:
|
|
|
430
425
|
|
|
431
426
|
:param workchain: Workchain identifier
|
|
432
427
|
:param shard: Shard identifier
|
|
433
|
-
:param seqno: Block
|
|
428
|
+
:param seqno: Block sequence number
|
|
434
429
|
:param lt: Logical time filter
|
|
435
430
|
:param utime: UNIX time filter
|
|
436
431
|
:param priority: Whether to use priority slot in the limiter
|
|
437
432
|
:return: Tuple of BlockIdExt and deserialized Block
|
|
438
433
|
"""
|
|
439
434
|
mode = 0
|
|
440
|
-
|
|
435
|
+
block_seqno = 0
|
|
436
|
+
|
|
437
|
+
if seqno is not None:
|
|
441
438
|
mode = 1
|
|
439
|
+
block_seqno = seqno
|
|
442
440
|
if lt is not None:
|
|
443
441
|
mode = 2
|
|
444
442
|
if utime is not None:
|
|
445
443
|
mode = 4
|
|
446
444
|
|
|
447
445
|
data = {
|
|
446
|
+
"mode": mode,
|
|
448
447
|
"id": {
|
|
449
|
-
"workchain": workchain
|
|
448
|
+
"workchain": workchain,
|
|
450
449
|
"shard": shard,
|
|
451
|
-
"seqno":
|
|
450
|
+
"seqno": block_seqno,
|
|
452
451
|
},
|
|
453
452
|
"lt": lt,
|
|
454
|
-
"mode": mode,
|
|
455
453
|
"utime": utime,
|
|
456
454
|
}
|
|
455
|
+
|
|
457
456
|
result = await self.send_liteserver_query(
|
|
458
|
-
|
|
457
|
+
"lookupBlock",
|
|
459
458
|
data=data,
|
|
460
459
|
priority=priority,
|
|
461
460
|
)
|
|
462
461
|
|
|
463
|
-
|
|
462
|
+
block_id = BlockIdExt.from_dict(result["id"])
|
|
463
|
+
header_proof = Cell.one_from_boc(result["header_proof"])
|
|
464
|
+
block = Block.deserialize(header_proof[0].begin_parse())
|
|
464
465
|
|
|
465
|
-
return
|
|
466
|
-
BlockIdExt.from_dict(result["id"]),
|
|
467
|
-
Block.deserialize(cell[0].begin_parse()),
|
|
468
|
-
)
|
|
466
|
+
return block_id, block
|
|
469
467
|
|
|
470
468
|
async def get_block_header(
|
|
471
469
|
self,
|
|
@@ -480,23 +478,25 @@ class AdnlProvider:
|
|
|
480
478
|
:param priority: Whether to use priority slot in the limiter
|
|
481
479
|
:return: Tuple of BlockIdExt and deserialized Block
|
|
482
480
|
"""
|
|
481
|
+
data = {"id": block.to_dict(), "mode": 0}
|
|
482
|
+
|
|
483
483
|
result = await self.send_liteserver_query(
|
|
484
|
-
|
|
485
|
-
data=
|
|
484
|
+
"getBlockHeader",
|
|
485
|
+
data=data,
|
|
486
486
|
priority=priority,
|
|
487
487
|
)
|
|
488
488
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
489
|
+
block_id = BlockIdExt.from_dict(result["id"])
|
|
490
|
+
header_proof = Cell.one_from_boc(result["header_proof"])
|
|
491
|
+
block_obj = Block.deserialize(header_proof[0].begin_parse())
|
|
492
|
+
|
|
493
|
+
return block_id, block_obj
|
|
494
494
|
|
|
495
495
|
async def get_block_transactions_ext(
|
|
496
496
|
self,
|
|
497
497
|
block: BlockIdExt,
|
|
498
|
-
*,
|
|
499
498
|
count: int = 1024,
|
|
499
|
+
*,
|
|
500
500
|
priority: bool = False,
|
|
501
501
|
) -> t.List[Transaction]:
|
|
502
502
|
"""
|
|
@@ -508,7 +508,6 @@ class AdnlProvider:
|
|
|
508
508
|
:return: List of deserialized Transaction objects
|
|
509
509
|
"""
|
|
510
510
|
mode = 39
|
|
511
|
-
|
|
512
511
|
result = await self.send_liteserver_query(
|
|
513
512
|
method="listBlockTransactionsExt",
|
|
514
513
|
data={
|
|
@@ -632,9 +631,7 @@ class AdnlProvider:
|
|
|
632
631
|
|
|
633
632
|
exit_code = result.get("exit_code")
|
|
634
633
|
if exit_code is None:
|
|
635
|
-
raise ProviderError(
|
|
636
|
-
f"runSmcMethod: missing `exit_code` in response ({self.node.endpoint})"
|
|
637
|
-
)
|
|
634
|
+
raise ProviderError("runSmcMethod: missing exit_code in response")
|
|
638
635
|
|
|
639
636
|
if exit_code != 0:
|
|
640
637
|
raise RunGetMethodError(
|
|
@@ -735,7 +732,7 @@ class AdnlProvider:
|
|
|
735
732
|
"""
|
|
736
733
|
if count > 16:
|
|
737
734
|
raise ClientError(
|
|
738
|
-
"
|
|
735
|
+
"get_transactions supports up to 16 transactions per request"
|
|
739
736
|
)
|
|
740
737
|
|
|
741
738
|
data = {
|
|
@@ -759,7 +756,7 @@ class AdnlProvider:
|
|
|
759
756
|
curr_hash = cell.get_hash(0).hex()
|
|
760
757
|
if curr_hash != prev_tr_hash:
|
|
761
758
|
raise ClientError(
|
|
762
|
-
"
|
|
759
|
+
f"transaction hash mismatch: "
|
|
763
760
|
f"expected {prev_tr_hash}, got {curr_hash}"
|
|
764
761
|
)
|
|
765
762
|
|
|
@@ -16,7 +16,7 @@ from pytoniq_core.crypto.ciphers import (
|
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
from tonutils.clients.adnl.provider.models import LiteServer
|
|
19
|
-
from tonutils.exceptions import TransportError
|
|
19
|
+
from tonutils.exceptions import TransportError, NotConnectedError
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class AdnlTcpTransport:
|
|
@@ -41,6 +41,7 @@ class AdnlTcpTransport:
|
|
|
41
41
|
:param node: Liteserver configuration with host, port, and public key
|
|
42
42
|
:param connect_timeout: Timeout in seconds for connection
|
|
43
43
|
"""
|
|
44
|
+
self.node = node
|
|
44
45
|
self.server = Server(
|
|
45
46
|
host=node.host,
|
|
46
47
|
port=node.port,
|
|
@@ -67,6 +68,14 @@ class AdnlTcpTransport:
|
|
|
67
68
|
"""Check if the transport is currently connected."""
|
|
68
69
|
return self._connected
|
|
69
70
|
|
|
71
|
+
def _error(self, operation: str, reason: str) -> TransportError:
|
|
72
|
+
"""Create TransportError with endpoint context."""
|
|
73
|
+
return TransportError(
|
|
74
|
+
endpoint=self.node.endpoint,
|
|
75
|
+
operation=operation,
|
|
76
|
+
reason=reason,
|
|
77
|
+
)
|
|
78
|
+
|
|
70
79
|
@staticmethod
|
|
71
80
|
def _build_frame(data: bytes) -> bytes:
|
|
72
81
|
"""
|
|
@@ -125,12 +134,12 @@ class AdnlTcpTransport:
|
|
|
125
134
|
async def _flush(self) -> None:
|
|
126
135
|
"""Flush the TCP write buffer."""
|
|
127
136
|
if self.writer is None:
|
|
128
|
-
raise
|
|
137
|
+
raise self._error("send", "writer not initialized")
|
|
129
138
|
try:
|
|
130
139
|
await self.writer.drain()
|
|
131
140
|
except ConnectionError as exc:
|
|
132
141
|
await self.close()
|
|
133
|
-
raise
|
|
142
|
+
raise self._error("send", "connection lost") from exc
|
|
134
143
|
|
|
135
144
|
def encrypt_frame(self, data: bytes) -> bytes:
|
|
136
145
|
"""
|
|
@@ -139,9 +148,7 @@ class AdnlTcpTransport:
|
|
|
139
148
|
:param data: Plaintext frame bytes
|
|
140
149
|
"""
|
|
141
150
|
if self.enc_cipher is None:
|
|
142
|
-
raise
|
|
143
|
-
"transport cipher: encryption cipher is not initialized"
|
|
144
|
-
)
|
|
151
|
+
raise self._error("encrypt", "cipher not initialized")
|
|
145
152
|
return aes_ctr_encrypt(self.enc_cipher, data)
|
|
146
153
|
|
|
147
154
|
def decrypt_frame(self, data: bytes) -> bytes:
|
|
@@ -151,15 +158,13 @@ class AdnlTcpTransport:
|
|
|
151
158
|
:param data: Encrypted frame bytes
|
|
152
159
|
"""
|
|
153
160
|
if self.dec_cipher is None:
|
|
154
|
-
raise
|
|
155
|
-
"transport cipher: decryption cipher is not initialized"
|
|
156
|
-
)
|
|
161
|
+
raise self._error("decrypt", "cipher not initialized")
|
|
157
162
|
return aes_ctr_decrypt(self.dec_cipher, data)
|
|
158
163
|
|
|
159
164
|
async def connect(self) -> None:
|
|
160
165
|
"""Establish encrypted connection to the liteserver."""
|
|
161
166
|
if self._connected:
|
|
162
|
-
raise
|
|
167
|
+
raise self._error("connect", "already connected")
|
|
163
168
|
|
|
164
169
|
self.loop = asyncio.get_running_loop()
|
|
165
170
|
|
|
@@ -169,14 +174,14 @@ class AdnlTcpTransport:
|
|
|
169
174
|
timeout=self.connect_timeout,
|
|
170
175
|
)
|
|
171
176
|
except asyncio.TimeoutError as exc:
|
|
172
|
-
raise
|
|
173
|
-
|
|
177
|
+
raise self._error(
|
|
178
|
+
"connect", f"timeout after {self.connect_timeout}s"
|
|
174
179
|
) from exc
|
|
175
180
|
except OSError as exc:
|
|
176
|
-
raise
|
|
181
|
+
raise self._error("connect", "connection refused") from exc
|
|
177
182
|
|
|
178
183
|
if self.writer is None or self.reader is None:
|
|
179
|
-
raise
|
|
184
|
+
raise self._error("connect", "stream init failed")
|
|
180
185
|
|
|
181
186
|
try:
|
|
182
187
|
handshake = self._build_handshake()
|
|
@@ -189,13 +194,12 @@ class AdnlTcpTransport:
|
|
|
189
194
|
timeout=self.connect_timeout,
|
|
190
195
|
)
|
|
191
196
|
except asyncio.IncompleteReadError as exc:
|
|
192
|
-
raise
|
|
193
|
-
f"transport handshake: remote closed connection"
|
|
194
|
-
) from exc
|
|
197
|
+
raise self._error("handshake", "remote closed") from exc
|
|
195
198
|
except asyncio.TimeoutError as exc:
|
|
196
|
-
raise
|
|
197
|
-
|
|
199
|
+
raise self._error(
|
|
200
|
+
"handshake", f"timeout after {self.connect_timeout}s"
|
|
198
201
|
) from exc
|
|
202
|
+
|
|
199
203
|
self._connected = True
|
|
200
204
|
self._reader_task = asyncio.create_task(
|
|
201
205
|
self.frame_reader_loop(),
|
|
@@ -214,7 +218,7 @@ class AdnlTcpTransport:
|
|
|
214
218
|
:param payload: Raw ADNL packet bytes
|
|
215
219
|
"""
|
|
216
220
|
if not self._connected or self.writer is None:
|
|
217
|
-
raise
|
|
221
|
+
raise NotConnectedError()
|
|
218
222
|
|
|
219
223
|
packet = self._build_frame(payload)
|
|
220
224
|
encrypted = self.encrypt_frame(packet)
|
|
@@ -229,7 +233,7 @@ class AdnlTcpTransport:
|
|
|
229
233
|
Blocks until a complete packet is available from the background reader.
|
|
230
234
|
"""
|
|
231
235
|
if not self._connected:
|
|
232
|
-
raise
|
|
236
|
+
raise NotConnectedError()
|
|
233
237
|
return await self._incoming.get()
|
|
234
238
|
|
|
235
239
|
async def read_frame(self, discard: bool = False) -> t.Optional[bytes]:
|
|
@@ -244,24 +248,24 @@ class AdnlTcpTransport:
|
|
|
244
248
|
:param discard: If True, validates but returns None (used for handshake ack)
|
|
245
249
|
"""
|
|
246
250
|
if self.reader is None:
|
|
247
|
-
raise
|
|
251
|
+
raise self._error("recv", "reader not initialized")
|
|
248
252
|
|
|
249
253
|
length_enc = await self.reader.readexactly(4)
|
|
250
254
|
length_dec = self.decrypt_frame(length_enc)
|
|
251
255
|
data_len = int.from_bytes(length_dec, "little")
|
|
252
256
|
|
|
253
257
|
if data_len <= 0:
|
|
254
|
-
raise
|
|
258
|
+
raise self._error("recv", f"invalid frame length: {data_len}")
|
|
255
259
|
|
|
256
260
|
data_enc = await self.reader.readexactly(data_len)
|
|
257
261
|
data = self.decrypt_frame(data_enc)
|
|
258
262
|
|
|
259
263
|
if len(data) < 32:
|
|
260
|
-
raise
|
|
264
|
+
raise self._error("recv", f"frame too short: {len(data)} bytes")
|
|
261
265
|
|
|
262
266
|
payload, checksum = data[:-32], data[-32:]
|
|
263
267
|
if hashlib.sha256(payload).digest() != checksum:
|
|
264
|
-
raise
|
|
268
|
+
raise self._error("recv", "checksum mismatch")
|
|
265
269
|
|
|
266
270
|
if discard:
|
|
267
271
|
return None
|
|
@@ -278,6 +282,7 @@ class AdnlTcpTransport:
|
|
|
278
282
|
except asyncio.CancelledError:
|
|
279
283
|
pass
|
|
280
284
|
except (
|
|
285
|
+
TransportError,
|
|
281
286
|
asyncio.IncompleteReadError,
|
|
282
287
|
ConnectionAbortedError,
|
|
283
288
|
ConnectionError,
|
tonutils/clients/base.py
CHANGED
|
@@ -272,7 +272,11 @@ class BaseClient(abc.ABC):
|
|
|
272
272
|
|
|
273
273
|
blen = len(domain) * 8
|
|
274
274
|
rlen = t.cast(int, res[0])
|
|
275
|
-
|
|
275
|
+
|
|
276
|
+
cell = res[1]
|
|
277
|
+
|
|
278
|
+
if cell is None:
|
|
279
|
+
return None
|
|
276
280
|
|
|
277
281
|
if rlen % 8 != 0 or rlen > blen:
|
|
278
282
|
raise ValueError(f"Invalid resolved length: result {rlen}, bytes {blen}.")
|
tonutils/exceptions.py
CHANGED
|
@@ -23,7 +23,30 @@ class TonutilsError(Exception):
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class TransportError(TonutilsError):
|
|
26
|
-
"""
|
|
26
|
+
"""Transport-level failure with structured context.
|
|
27
|
+
|
|
28
|
+
Covers: TCP connect, ADNL handshake, send/recv, crypto failures.
|
|
29
|
+
|
|
30
|
+
:param endpoint: Server address as "host:port"
|
|
31
|
+
:param operation: What was attempted ("connect", "handshake", "send", "recv")
|
|
32
|
+
:param reason: Why it failed ("timeout 2.0s", "connection refused", etc.)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
endpoint: str
|
|
36
|
+
operation: str
|
|
37
|
+
reason: str
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
endpoint: str,
|
|
43
|
+
operation: str,
|
|
44
|
+
reason: str,
|
|
45
|
+
) -> None:
|
|
46
|
+
self.endpoint = endpoint
|
|
47
|
+
self.operation = operation
|
|
48
|
+
self.reason = reason
|
|
49
|
+
super().__init__(f"{operation} failed at {endpoint}: {reason}")
|
|
27
50
|
|
|
28
51
|
|
|
29
52
|
class ProviderError(TonutilsError):
|
|
@@ -39,23 +62,24 @@ class BalancerError(TonutilsError):
|
|
|
39
62
|
|
|
40
63
|
|
|
41
64
|
class NotConnectedError(TonutilsError, RuntimeError):
|
|
42
|
-
"""Raise when an operation requires an active connection.
|
|
65
|
+
"""Raise when an operation requires an active connection."""
|
|
43
66
|
|
|
44
|
-
|
|
45
|
-
"""
|
|
67
|
+
endpoint: t.Optional[str]
|
|
46
68
|
|
|
47
|
-
def __init__(self) -> None:
|
|
48
|
-
|
|
69
|
+
def __init__(self, endpoint: t.Optional[str] = None) -> None:
|
|
70
|
+
self.endpoint = endpoint
|
|
71
|
+
if endpoint:
|
|
72
|
+
super().__init__(f"not connected to {endpoint}")
|
|
73
|
+
else:
|
|
74
|
+
super().__init__("not connected")
|
|
49
75
|
|
|
50
76
|
|
|
51
77
|
class ProviderTimeoutError(ProviderError, asyncio.TimeoutError):
|
|
52
78
|
"""Raise when a provider operation exceeds its timeout.
|
|
53
79
|
|
|
54
|
-
Used for both ADNL and HTTP providers.
|
|
55
|
-
|
|
56
80
|
:param timeout: Timeout in seconds.
|
|
57
81
|
:param endpoint: Endpoint identifier (URL or host:port).
|
|
58
|
-
:param operation: Operation label (e.g. "
|
|
82
|
+
:param operation: Operation label (e.g. "request", "connect").
|
|
59
83
|
"""
|
|
60
84
|
|
|
61
85
|
timeout: float
|
|
@@ -63,19 +87,15 @@ class ProviderTimeoutError(ProviderError, asyncio.TimeoutError):
|
|
|
63
87
|
operation: str
|
|
64
88
|
|
|
65
89
|
def __init__(self, *, timeout: float, endpoint: str, operation: str) -> None:
|
|
66
|
-
self.timeout =
|
|
90
|
+
self.timeout = timeout
|
|
67
91
|
self.endpoint = endpoint
|
|
68
92
|
self.operation = operation
|
|
69
|
-
super().__init__(f"{operation} timed out after {timeout}s
|
|
93
|
+
super().__init__(f"{operation} timed out after {timeout}s at {endpoint}")
|
|
70
94
|
|
|
71
95
|
|
|
72
96
|
class ProviderResponseError(ProviderError):
|
|
73
97
|
"""Raise when a backend returns an error response.
|
|
74
98
|
|
|
75
|
-
This is a normalized provider error for:
|
|
76
|
-
- HTTP status codes (e.g. 429/5xx)
|
|
77
|
-
- lite-server numeric error codes
|
|
78
|
-
|
|
79
99
|
:param code: Backend code (HTTP status or lite-server code).
|
|
80
100
|
:param message: Backend error description.
|
|
81
101
|
:param endpoint: Endpoint identifier (URL or host:port).
|
|
@@ -86,7 +106,7 @@ class ProviderResponseError(ProviderError):
|
|
|
86
106
|
endpoint: str
|
|
87
107
|
|
|
88
108
|
def __init__(self, *, code: int, message: str, endpoint: str) -> None:
|
|
89
|
-
self.code =
|
|
109
|
+
self.code = code
|
|
90
110
|
self.message = message
|
|
91
111
|
self.endpoint = endpoint
|
|
92
112
|
super().__init__(f"request failed with code {code} at {endpoint}: {message}")
|
|
@@ -111,13 +131,10 @@ class RetryLimitError(ProviderError):
|
|
|
111
131
|
max_attempts: int,
|
|
112
132
|
last_error: ProviderError,
|
|
113
133
|
) -> None:
|
|
114
|
-
self.attempts =
|
|
115
|
-
self.max_attempts =
|
|
134
|
+
self.attempts = attempts
|
|
135
|
+
self.max_attempts = max_attempts
|
|
116
136
|
self.last_error = last_error
|
|
117
|
-
super().__init__(
|
|
118
|
-
f"retry exhausted ({self.attempts}/{self.max_attempts}). "
|
|
119
|
-
f"Last error: {last_error}"
|
|
120
|
-
)
|
|
137
|
+
super().__init__(f"retry exhausted ({attempts}/{max_attempts}): {last_error}")
|
|
121
138
|
|
|
122
139
|
|
|
123
140
|
class ContractError(ClientError):
|
|
@@ -133,7 +150,6 @@ class ContractError(ClientError):
|
|
|
133
150
|
def __init__(self, target: t.Any, message: str) -> None:
|
|
134
151
|
self.target = target
|
|
135
152
|
self.message = message
|
|
136
|
-
|
|
137
153
|
name = (
|
|
138
154
|
target.__name__ if isinstance(target, type) else target.__class__.__name__
|
|
139
155
|
)
|
|
@@ -143,10 +159,6 @@ class ContractError(ClientError):
|
|
|
143
159
|
class StateNotLoadedError(ContractError):
|
|
144
160
|
"""Raise when a contract wrapper requires state that is not loaded.
|
|
145
161
|
|
|
146
|
-
Typical cases:
|
|
147
|
-
- state_info not fetched
|
|
148
|
-
- state_data not decoded/available
|
|
149
|
-
|
|
150
162
|
:param contract: Contract instance related to the failure.
|
|
151
163
|
:param missing: Missing field name (e.g. "state_info", "state_data").
|
|
152
164
|
"""
|
|
@@ -174,9 +186,9 @@ class RunGetMethodError(ClientError):
|
|
|
174
186
|
def __init__(self, *, address: str, method_name: str, exit_code: int) -> None:
|
|
175
187
|
self.address = address
|
|
176
188
|
self.method_name = method_name
|
|
177
|
-
self.exit_code =
|
|
189
|
+
self.exit_code = exit_code
|
|
178
190
|
super().__init__(
|
|
179
|
-
f"get-method
|
|
191
|
+
f"get-method '{method_name}' failed for {address} with exit code {exit_code}"
|
|
180
192
|
)
|
|
181
193
|
|
|
182
194
|
|
|
@@ -198,5 +210,3 @@ CDN_CHALLENGE_MARKERS: t.Dict[str, str] = {
|
|
|
198
210
|
"503 service unavailable": "Service temporarily unavailable (proxy or CDN).",
|
|
199
211
|
"ddos": "Possible DDoS protection or mitigation page.",
|
|
200
212
|
}
|
|
201
|
-
"""Markers for detecting CDN / proxy challenge and anti-DDoS responses,
|
|
202
|
-
used for error normalization and default retry policies."""
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .events import (
|
|
2
|
+
BlockEvent,
|
|
3
|
+
TransactionEvent,
|
|
4
|
+
TransactionsEvent,
|
|
5
|
+
)
|
|
6
|
+
from .scanner import BlockScanner
|
|
7
|
+
from .where import Where
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"BlockScanner",
|
|
12
|
+
"BlockEvent",
|
|
13
|
+
"TransactionEvent",
|
|
14
|
+
"TransactionsEvent",
|
|
15
|
+
"Where",
|
|
16
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
from tonutils.tools.block_scanner.events import (
|
|
4
|
+
BlockEvent,
|
|
5
|
+
EventBase,
|
|
6
|
+
TransactionEvent,
|
|
7
|
+
TransactionsEvent,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
TEvent = t.TypeVar("TEvent", bound=EventBase)
|
|
11
|
+
|
|
12
|
+
Handler = t.Callable[[TEvent], t.Awaitable[None]]
|
|
13
|
+
Where = t.Callable[[TEvent], t.Union[bool, t.Awaitable[bool]]]
|
|
14
|
+
|
|
15
|
+
BlockWhere = t.Callable[[BlockEvent], t.Union[bool, t.Awaitable[bool]]]
|
|
16
|
+
TransactionWhere = t.Callable[[TransactionEvent], t.Union[bool, t.Awaitable[bool]]]
|
|
17
|
+
TransactionsWhere = t.Callable[[TransactionsEvent], t.Union[bool, t.Awaitable[bool]]]
|
|
18
|
+
|
|
19
|
+
AnyHandler = t.Callable[[EventBase], t.Awaitable[None]]
|
|
20
|
+
AnyWhere = t.Callable[[EventBase], t.Union[bool, t.Awaitable[bool]]]
|
|
21
|
+
|
|
22
|
+
HandlerEntry = t.Tuple[AnyHandler, t.Optional[AnyWhere]]
|
|
23
|
+
Decorator = t.Callable[[Handler[TEvent]], Handler[TEvent]]
|