tonutils 2.0.1b2__py3-none-any.whl → 2.0.1b4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. tonutils/__init__.py +0 -2
  2. tonutils/__meta__.py +1 -1
  3. tonutils/cli.py +111 -0
  4. tonutils/clients/__init__.py +7 -11
  5. tonutils/clients/adnl/__init__.py +7 -3
  6. tonutils/clients/adnl/balancer.py +362 -168
  7. tonutils/clients/adnl/client.py +203 -67
  8. tonutils/clients/adnl/provider/config.py +24 -25
  9. tonutils/clients/adnl/provider/models.py +4 -0
  10. tonutils/clients/adnl/provider/provider.py +203 -160
  11. tonutils/clients/adnl/provider/transport.py +44 -33
  12. tonutils/clients/adnl/provider/workers/base.py +0 -2
  13. tonutils/clients/adnl/provider/workers/pinger.py +1 -1
  14. tonutils/clients/adnl/provider/workers/reader.py +3 -2
  15. tonutils/clients/adnl/{provider/builder.py → utils.py} +62 -2
  16. tonutils/clients/http/__init__.py +11 -8
  17. tonutils/clients/http/balancer.py +75 -63
  18. tonutils/clients/http/clients/__init__.py +13 -0
  19. tonutils/clients/http/clients/chainstack.py +48 -0
  20. tonutils/clients/http/clients/quicknode.py +47 -0
  21. tonutils/clients/http/clients/tatum.py +56 -0
  22. tonutils/clients/http/{tonapi/client.py → clients/tonapi.py} +31 -31
  23. tonutils/clients/http/{toncenter/client.py → clients/toncenter.py} +59 -48
  24. tonutils/clients/http/providers/__init__.py +4 -0
  25. tonutils/clients/http/providers/base.py +201 -0
  26. tonutils/clients/http/providers/response.py +85 -0
  27. tonutils/clients/http/providers/tonapi/__init__.py +3 -0
  28. tonutils/clients/http/{tonapi → providers/tonapi}/models.py +1 -0
  29. tonutils/clients/http/providers/tonapi/provider.py +125 -0
  30. tonutils/clients/http/providers/toncenter/__init__.py +3 -0
  31. tonutils/clients/http/{toncenter → providers/toncenter}/models.py +1 -0
  32. tonutils/clients/http/providers/toncenter/provider.py +119 -0
  33. tonutils/clients/http/utils.py +140 -0
  34. tonutils/clients/limiter.py +115 -0
  35. tonutils/contracts/__init__.py +4 -0
  36. tonutils/contracts/base.py +33 -20
  37. tonutils/contracts/dns/methods.py +2 -2
  38. tonutils/contracts/jetton/methods.py +2 -2
  39. tonutils/contracts/nft/methods.py +2 -2
  40. tonutils/contracts/nft/tlb.py +1 -1
  41. tonutils/{protocols/contract.py → contracts/protocol.py} +29 -29
  42. tonutils/contracts/telegram/methods.py +2 -2
  43. tonutils/contracts/vanity/vanity.py +1 -1
  44. tonutils/contracts/wallet/__init__.py +2 -0
  45. tonutils/contracts/wallet/base.py +3 -3
  46. tonutils/contracts/wallet/messages.py +1 -1
  47. tonutils/contracts/wallet/methods.py +2 -2
  48. tonutils/{protocols/wallet.py → contracts/wallet/protocol.py} +35 -35
  49. tonutils/contracts/wallet/versions/v5.py +3 -3
  50. tonutils/exceptions.py +146 -228
  51. tonutils/tonconnect/__init__.py +0 -0
  52. tonutils/tools/__init__.py +6 -0
  53. tonutils/tools/block_scanner/__init__.py +26 -0
  54. tonutils/tools/block_scanner/annotations.py +23 -0
  55. tonutils/tools/block_scanner/dispatcher.py +141 -0
  56. tonutils/tools/block_scanner/events.py +31 -0
  57. tonutils/tools/block_scanner/scanner.py +315 -0
  58. tonutils/tools/block_scanner/traversal.py +96 -0
  59. tonutils/tools/block_scanner/where.py +151 -0
  60. tonutils/tools/status_monitor/__init__.py +3 -0
  61. tonutils/tools/status_monitor/console.py +157 -0
  62. tonutils/tools/status_monitor/models.py +27 -0
  63. tonutils/tools/status_monitor/monitor.py +295 -0
  64. tonutils/types.py +125 -2
  65. tonutils/utils.py +3 -3
  66. {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b4.dist-info}/METADATA +2 -5
  67. tonutils-2.0.1b4.dist-info/RECORD +108 -0
  68. tonutils-2.0.1b4.dist-info/entry_points.txt +2 -0
  69. tonutils/clients/adnl/provider/limiter.py +0 -56
  70. tonutils/clients/adnl/stack.py +0 -64
  71. tonutils/clients/http/chainstack/__init__.py +0 -4
  72. tonutils/clients/http/chainstack/client.py +0 -63
  73. tonutils/clients/http/chainstack/provider.py +0 -44
  74. tonutils/clients/http/quicknode/__init__.py +0 -4
  75. tonutils/clients/http/quicknode/client.py +0 -60
  76. tonutils/clients/http/quicknode/provider.py +0 -42
  77. tonutils/clients/http/tatum/__init__.py +0 -4
  78. tonutils/clients/http/tatum/client.py +0 -66
  79. tonutils/clients/http/tatum/provider.py +0 -53
  80. tonutils/clients/http/tonapi/__init__.py +0 -4
  81. tonutils/clients/http/tonapi/provider.py +0 -150
  82. tonutils/clients/http/tonapi/stack.py +0 -71
  83. tonutils/clients/http/toncenter/__init__.py +0 -4
  84. tonutils/clients/http/toncenter/provider.py +0 -145
  85. tonutils/clients/http/toncenter/stack.py +0 -73
  86. tonutils/protocols/__init__.py +0 -9
  87. tonutils-2.0.1b2.dist-info/RECORD +0 -98
  88. /tonutils/{protocols/client.py → clients/protocol.py} +0 -0
  89. {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b4.dist-info}/WHEEL +0 -0
  90. {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b4.dist-info}/licenses/LICENSE +0 -0
  91. {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,157 @@
1
+ import sys
2
+ import typing as t
3
+ from collections import deque
4
+ from datetime import datetime
5
+
6
+ from tonutils.tools.status_monitor.models import BlockInfo, LiteServerStatus
7
+
8
+ _ENTER_ALT_SCREEN = "\033[?1049h"
9
+ _EXIT_ALT_SCREEN = "\033[?1049l"
10
+ _HIDE_CURSOR = "\033[?25l"
11
+ _SHOW_CURSOR = "\033[?25h"
12
+ _MOVE_HOME = "\033[H"
13
+ _CLEAR_SCREEN = "\033[2J"
14
+ _CLEAR_LINE = "\033[K"
15
+
16
+
17
+ class Console:
18
+ HEADERS = [
19
+ "LS",
20
+ "HOST",
21
+ "PORT",
22
+ "Version",
23
+ "Time",
24
+ "Ping",
25
+ "Connect RTT",
26
+ "Request RTT",
27
+ "Last MC Block",
28
+ "Last BC Block",
29
+ "Archive From",
30
+ ]
31
+ WIDTHS = [2, 15, 5, 7, 19, 7, 11, 11, 16, 16, 12]
32
+
33
+ TABLE_TITLE = "Lite Server Status"
34
+ ERROR_TITLE = "Error Log"
35
+ MAX_ERROR_LOGS = 10
36
+
37
+ def __init__(self) -> None:
38
+ self._index_width = 2
39
+ self._is_tty = sys.stdout.isatty()
40
+ self._error_log: deque[str] = deque(maxlen=self.MAX_ERROR_LOGS)
41
+ self._prev_errors: t.Dict[int, t.Optional[str]] = {}
42
+
43
+ def enter(self) -> None:
44
+ if self._is_tty:
45
+ sys.stdout.write(_ENTER_ALT_SCREEN + _HIDE_CURSOR + _CLEAR_SCREEN)
46
+ sys.stdout.flush()
47
+
48
+ def exit(self) -> None:
49
+ if self._is_tty:
50
+ sys.stdout.write(_SHOW_CURSOR + _EXIT_ALT_SCREEN)
51
+ sys.stdout.flush()
52
+
53
+ def render(self, statuses: t.List[LiteServerStatus]) -> None:
54
+ self._update_state(statuses)
55
+ self._home()
56
+ self._draw(statuses)
57
+
58
+ def _home(self) -> None:
59
+ if self._is_tty:
60
+ sys.stdout.write(_MOVE_HOME)
61
+ sys.stdout.flush()
62
+
63
+ def _update_state(self, statuses: t.List[LiteServerStatus]) -> None:
64
+ self._update_index_width(statuses)
65
+ self._update_error_log(statuses)
66
+
67
+ def _update_index_width(self, statuses: t.List[LiteServerStatus]) -> None:
68
+ if statuses:
69
+ self._index_width = max(2, len(str(len(statuses) - 1)))
70
+
71
+ def _update_error_log(self, statuses: t.List[LiteServerStatus]) -> None:
72
+ now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
73
+ for status in statuses:
74
+ prev = self._prev_errors.get(status.server.index)
75
+ if status.last_error and status.last_error != prev:
76
+ idx = str(status.server.index).rjust(self._index_width)
77
+ self._error_log.appendleft(f" {now} [LS {idx}]: {status.last_error}")
78
+ self._prev_errors[status.server.index] = status.last_error
79
+
80
+ def _get_table_width(self) -> int:
81
+ return sum(self.WIDTHS) + (len(self.WIDTHS) - 1) * 3
82
+
83
+ def _draw(self, statuses: t.List[LiteServerStatus]) -> None:
84
+ table_width = self._get_table_width()
85
+ padding = (table_width - len(self.TABLE_TITLE)) // 2
86
+
87
+ lines = [
88
+ "═" * table_width,
89
+ " " * padding + self.TABLE_TITLE,
90
+ "═" * table_width,
91
+ self._format_header(),
92
+ self._format_separator(),
93
+ ]
94
+
95
+ for status in statuses:
96
+ lines.append(self._format_row(status))
97
+
98
+ lines.append("")
99
+
100
+ if self._error_log:
101
+ lines.append("─" * table_width)
102
+ lines.append(f" {self.ERROR_TITLE}:")
103
+ lines.extend(self._error_log)
104
+
105
+ output = (_CLEAR_LINE + "\n").join(lines) + _CLEAR_LINE
106
+
107
+ output += "\n" + _CLEAR_LINE
108
+ sys.stdout.write(output)
109
+ sys.stdout.flush()
110
+
111
+ def _format_header(self) -> str:
112
+ return " │ ".join(h.ljust(w) for h, w in zip(self.HEADERS, self.WIDTHS))
113
+
114
+ def _format_separator(self) -> str:
115
+ return "─┼─".join("─" * w for w in self.WIDTHS)
116
+
117
+ def _format_row(self, status: LiteServerStatus) -> str:
118
+ cells = [
119
+ str(status.server.index),
120
+ status.server.host,
121
+ str(status.server.port),
122
+ self._fmt_int(status.version),
123
+ self._fmt_datetime(status.time),
124
+ self._fmt_ms(status.ping_ms),
125
+ self._fmt_ms(status.connect_ms),
126
+ self._fmt_ms(status.request_ms),
127
+ self._fmt_block(status.last_mc_block),
128
+ self._fmt_block(status.last_bc_block),
129
+ self._fmt_date(status.archive_from),
130
+ ]
131
+ return " │ ".join(c.ljust(w) for c, w in zip(cells, self.WIDTHS))
132
+
133
+ @staticmethod
134
+ def _fmt_int(value: t.Optional[int]) -> str:
135
+ return str(value) if value is not None else "-"
136
+
137
+ @staticmethod
138
+ def _fmt_ms(value: t.Optional[int]) -> str:
139
+ return f"{value}ms" if value is not None else "-"
140
+
141
+ @staticmethod
142
+ def _fmt_block(block: t.Optional[BlockInfo]) -> str:
143
+ if block is None:
144
+ return "-"
145
+ return f"{block.seqno} / {block.txs_count}"
146
+
147
+ @staticmethod
148
+ def _fmt_date(ts: t.Optional[int]) -> str:
149
+ if ts is None:
150
+ return "-"
151
+ return datetime.fromtimestamp(ts).strftime("%Y-%m-%d")
152
+
153
+ @staticmethod
154
+ def _fmt_datetime(ts: t.Optional[int]) -> str:
155
+ if ts is None:
156
+ return "-"
157
+ return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
@@ -0,0 +1,27 @@
1
+ import typing as t
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class LiteServer(BaseModel):
7
+ index: int
8
+ host: str
9
+ port: int
10
+
11
+
12
+ class BlockInfo(BaseModel):
13
+ seqno: int
14
+ txs_count: int
15
+
16
+
17
+ class LiteServerStatus(BaseModel):
18
+ server: LiteServer
19
+ version: t.Optional[int] = None
20
+ time: t.Optional[int] = None
21
+ ping_ms: t.Optional[int] = None
22
+ connect_ms: t.Optional[int] = None
23
+ request_ms: t.Optional[int] = None
24
+ last_mc_block: t.Optional[BlockInfo] = None
25
+ last_bc_block: t.Optional[BlockInfo] = None
26
+ archive_from: t.Optional[int] = None
27
+ last_error: t.Optional[str] = None
@@ -0,0 +1,295 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import time
5
+ import typing as t
6
+
7
+ from tonutils.clients import LiteClient
8
+ from tonutils.clients.adnl.provider.models import GlobalConfig
9
+ from tonutils.tools.status_monitor.console import Console
10
+ from tonutils.tools.status_monitor.models import (
11
+ BlockInfo,
12
+ LiteServerStatus,
13
+ LiteServer,
14
+ )
15
+ from tonutils.types import (
16
+ NetworkGlobalID,
17
+ WorkchainID,
18
+ MAINNET_GENESIS_UTIME,
19
+ MASTERCHAIN_SHARD,
20
+ )
21
+
22
+
23
+ class LiteServerMonitor:
24
+ RENDER_INTERVAL = 0.1
25
+ RECONNECT_INTERVAL = 30.0
26
+
27
+ FAST_UPDATE_INTERVAL = 0.3
28
+ MEDIUM_UPDATE_INTERVAL = 3.0
29
+ SLOW_UPDATE_INTERVAL = 10.0
30
+
31
+ def __init__(self, clients: t.List[LiteClient]) -> None:
32
+ self._clients = clients
33
+ self._console = Console()
34
+
35
+ self._archive_cache: t.Dict[int, int] = {}
36
+ self._statuses: t.Dict[int, LiteServerStatus] = {}
37
+ self._last_connect: t.Dict[int, float] = {}
38
+
39
+ self._tasks: t.List[asyncio.Task[None]] = []
40
+ self._stop = asyncio.Event()
41
+
42
+ self._locks: t.Dict[int, asyncio.Lock] = {}
43
+
44
+ @classmethod
45
+ def from_config(
46
+ cls,
47
+ config: GlobalConfig,
48
+ network: NetworkGlobalID,
49
+ rps_limit: t.Optional[int] = 100,
50
+ ) -> LiteServerMonitor:
51
+ return cls(
52
+ [
53
+ LiteClient(
54
+ network=network,
55
+ ip=server.host,
56
+ port=server.port,
57
+ public_key=server.id,
58
+ rps_limit=rps_limit,
59
+ )
60
+ for server in config.liteservers
61
+ ]
62
+ )
63
+
64
+ @property
65
+ def statuses(self) -> t.List[LiteServerStatus]:
66
+ return list(self._statuses.values())
67
+
68
+ async def run(self) -> None:
69
+ self._console.enter()
70
+ self._init_statuses()
71
+ self._start_update_loops()
72
+
73
+ try:
74
+ while not self._stop.is_set():
75
+ self._console.render(self.statuses)
76
+ await self._sleep(self.RENDER_INTERVAL)
77
+ finally:
78
+ self._console.exit()
79
+
80
+ async def stop(self) -> None:
81
+ if self._stop.is_set():
82
+ return
83
+ self._stop.set()
84
+
85
+ for task in self._tasks:
86
+ task.cancel()
87
+ await asyncio.gather(*self._tasks, return_exceptions=True)
88
+
89
+ close_tasks = [client.close() for client in self._clients]
90
+ await asyncio.gather(*close_tasks, return_exceptions=True)
91
+
92
+ def _init_statuses(self) -> None:
93
+ for index, client in enumerate(self._clients):
94
+ server = LiteServer(
95
+ index=index,
96
+ host=client.provider.node.host,
97
+ port=client.provider.node.port,
98
+ )
99
+ self._statuses[index] = LiteServerStatus(server=server)
100
+ self._locks[index] = asyncio.Lock()
101
+
102
+ def _start_update_loops(self) -> None:
103
+ if self._tasks:
104
+ return
105
+
106
+ for index, client in enumerate(self._clients):
107
+ fast = self._fast_update_loop(index, client)
108
+ self._tasks.append(asyncio.create_task(fast))
109
+
110
+ medium = self._medium_update_loop(index, client)
111
+ self._tasks.append(asyncio.create_task(medium))
112
+
113
+ slow = self._slow_update_loop(index, client)
114
+ self._tasks.append(asyncio.create_task(slow))
115
+
116
+ async def _ensure_connected(self, index: int, client: LiteClient) -> bool:
117
+ if client.provider.is_connected:
118
+ return True
119
+
120
+ now = time.monotonic()
121
+ last_attempt = self._last_connect.get(index, 0.0)
122
+ if now - last_attempt < self.RECONNECT_INTERVAL:
123
+ return False
124
+
125
+ self._last_connect[index] = now
126
+ await self._connect(index, client)
127
+ return client.provider.is_connected
128
+
129
+ async def _fast_update_loop(self, index: int, client: LiteClient) -> None:
130
+ while not self._stop.is_set():
131
+ if not await self._ensure_connected(index, client):
132
+ await self._sleep(1.0)
133
+ continue
134
+
135
+ await asyncio.gather(
136
+ self._update_time(index, client),
137
+ self._update_last_blocks(index, client),
138
+ return_exceptions=True,
139
+ )
140
+ await self._sleep(self.FAST_UPDATE_INTERVAL)
141
+
142
+ async def _medium_update_loop(self, index: int, client: LiteClient) -> None:
143
+ while not self._stop.is_set():
144
+ if not client.is_connected:
145
+ await self._sleep(1.0)
146
+ continue
147
+
148
+ await asyncio.gather(
149
+ self._update_ping_ms(index, client),
150
+ self._update_request_ms(index, client),
151
+ return_exceptions=True,
152
+ )
153
+ await self._sleep(self.MEDIUM_UPDATE_INTERVAL)
154
+
155
+ async def _slow_update_loop(self, index: int, client: LiteClient) -> None:
156
+ while not self._stop.is_set():
157
+ if not client.is_connected:
158
+ await self._sleep(1.0)
159
+ continue
160
+
161
+ await asyncio.gather(
162
+ self._update_version(index, client),
163
+ self._update_archive_from(index, client),
164
+ return_exceptions=True,
165
+ )
166
+ await self._sleep(self.SLOW_UPDATE_INTERVAL)
167
+
168
+ async def _sleep(self, seconds: float) -> None:
169
+ try:
170
+ await asyncio.wait_for(self._stop.wait(), timeout=seconds)
171
+ except asyncio.TimeoutError:
172
+ pass
173
+
174
+ async def _set_status(self, index: int, **kwargs: t.Any) -> None:
175
+ async with self._locks[index]:
176
+ current = self._statuses[index]
177
+ self._statuses[index] = current.model_copy(update=kwargs)
178
+
179
+ async def _connect(self, index: int, client: LiteClient) -> None:
180
+ try:
181
+ start = time.perf_counter()
182
+ await client.connect()
183
+ connect_ms = int((time.perf_counter() - start) * 1000)
184
+ await self._set_status(index, connect_ms=connect_ms, last_error=None)
185
+ except Exception as e:
186
+ await self._set_status(index, last_error=str(e))
187
+
188
+ async def _update_version(self, index: int, client: LiteClient) -> None:
189
+ try:
190
+ version = await client.get_version()
191
+ await self._set_status(index, version=version)
192
+ except Exception as e:
193
+ await self._set_status(index, last_error=str(e))
194
+
195
+ async def _update_time(self, index: int, client: LiteClient) -> None:
196
+ try:
197
+ server_time = await client.get_time()
198
+ await self._set_status(index, time=server_time)
199
+ except Exception as e:
200
+ await self._set_status(index, last_error=str(e))
201
+
202
+ async def _update_ping_ms(self, index: int, client: LiteClient) -> None:
203
+ try:
204
+ ping_ms = client.provider.last_ping_ms
205
+ if ping_ms is not None:
206
+ await self._set_status(index, ping_ms=ping_ms)
207
+ except Exception as e:
208
+ await self._set_status(index, last_error=str(e))
209
+
210
+ async def _update_request_ms(self, index: int, client: LiteClient) -> None:
211
+ try:
212
+ start = time.perf_counter()
213
+ await client.get_masterchain_info()
214
+ request_ms = int((time.perf_counter() - start) * 1000)
215
+ await self._set_status(index, request_ms=request_ms)
216
+ except Exception as e:
217
+ await self._set_status(index, last_error=str(e))
218
+
219
+ async def _update_last_blocks(self, index: int, client: LiteClient) -> None:
220
+ try:
221
+ mc_block = client.provider.last_mc_block
222
+ if mc_block is None:
223
+ return
224
+
225
+ mc_txs, shards = await asyncio.gather(
226
+ client.get_block_transactions_ext(mc_block),
227
+ client.get_all_shards_info(mc_block),
228
+ )
229
+ last_mc_block = BlockInfo(seqno=mc_block.seqno, txs_count=len(mc_txs))
230
+
231
+ if shards:
232
+ bc_block = max(shards, key=lambda b: b.seqno)
233
+ bc_txs = await client.get_block_transactions_ext(bc_block)
234
+ last_bc_block = BlockInfo(seqno=bc_block.seqno, txs_count=len(bc_txs))
235
+ await self._set_status(
236
+ index,
237
+ last_mc_block=last_mc_block,
238
+ last_bc_block=last_bc_block,
239
+ )
240
+ else:
241
+ await self._set_status(index, last_mc_block=last_mc_block)
242
+
243
+ except Exception as e:
244
+ await self._set_status(index, last_error=str(e))
245
+
246
+ async def _update_archive_from(self, index: int, client: LiteClient) -> None:
247
+ try:
248
+ now = int(time.time())
249
+ result = await self._find_archive_depth(
250
+ client, now, self._archive_cache.get(index)
251
+ )
252
+ self._archive_cache[index] = result
253
+ await self._set_status(index, archive_from=result)
254
+ except Exception as e:
255
+ await self._set_status(index, last_error=str(e))
256
+
257
+ @staticmethod
258
+ async def _find_archive_depth(
259
+ client: LiteClient,
260
+ now: int,
261
+ cached: t.Optional[int] = None,
262
+ ) -> int:
263
+ seconds_per_day = 86400
264
+ seconds_diff = now - MAINNET_GENESIS_UTIME
265
+ right = seconds_diff // seconds_per_day
266
+
267
+ if cached is not None:
268
+ cached_days = (now - cached) // seconds_per_day
269
+ left = cached_days
270
+ best_days = cached_days
271
+ else:
272
+ left = 0
273
+ best_days = 0
274
+
275
+ async def probe(days: int) -> bool:
276
+ utime = now - days * seconds_per_day
277
+ try:
278
+ await client.provider.lookup_block(
279
+ workchain=WorkchainID.MASTERCHAIN,
280
+ shard=MASTERCHAIN_SHARD,
281
+ utime=utime,
282
+ )
283
+ return True
284
+ except (Exception,):
285
+ return False
286
+
287
+ while left <= right:
288
+ mid = (left + right) // 2
289
+ if await probe(mid):
290
+ best_days = mid
291
+ left = mid + 1
292
+ else:
293
+ right = mid - 1
294
+
295
+ return now - best_days * seconds_per_day
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,21 +17,29 @@ __all__ = [
16
17
  "ClientType",
17
18
  "ContractState",
18
19
  "ContractStateInfo",
19
- "DEFAULT_SENDMODE",
20
- "DEFAULT_SUBWALLET_ID",
21
20
  "DNSCategory",
22
21
  "DNSPrefix",
23
22
  "MetadataPrefix",
24
23
  "NetworkGlobalID",
25
24
  "PrivateKey",
26
25
  "PublicKey",
26
+ "RetryPolicy",
27
+ "RetryRule",
27
28
  "SendMode",
28
29
  "StackItem",
29
30
  "StackItems",
30
31
  "StackTag",
31
32
  "WorkchainID",
33
+ "DEFAULT_ADNL_RETRY_POLICY",
34
+ "DEFAULT_HTTP_RETRY_POLICY",
35
+ "DEFAULT_SENDMODE",
36
+ "DEFAULT_SUBWALLET_ID",
37
+ "MAINNET_GENESIS_UTIME",
38
+ "MASTERCHAIN_SHARD",
32
39
  ]
33
40
 
41
+ from tonutils.exceptions import CDN_CHALLENGE_MARKERS
42
+
34
43
  AddressLike = t.Union[Address, str]
35
44
  """Type alias for TON address inputs. Accepts either an Address object or string representation."""
36
45
 
@@ -375,8 +384,122 @@ class BagID(ADNL):
375
384
  """TON Storage bag identifier (32 bytes)."""
376
385
 
377
386
 
387
+ @dataclass(slots=True, frozen=True)
388
+ class RetryRule:
389
+ """
390
+ Retry rule matched by numeric code and/or message substrings.
391
+
392
+ Matching:
393
+ - if codes is set: code must be in codes
394
+ - if markers is set: any marker must be present in message (case-insensitive)
395
+ - if both are set: both conditions must match
396
+
397
+ Attributes:
398
+ attempts: Maximum number of retry attempts
399
+ base_delay: Initial delay before first retry (seconds)
400
+ cap_delay: Maximum delay between retries (seconds)
401
+ codes: Error or status codes this rule applies to
402
+ markers: Case-insensitive substrings matched against error message
403
+ """
404
+
405
+ attempts: int = 3
406
+ base_delay: float = 0.3
407
+ cap_delay: float = 3.0
408
+
409
+ codes: t.Optional[t.Tuple[int, ...]] = None
410
+ markers: t.Optional[t.Tuple[str, ...]] = None
411
+
412
+ def __post_init__(self) -> None:
413
+ if self.attempts < 1:
414
+ raise ValueError("attempts must be >= 1")
415
+ if self.base_delay < 0:
416
+ raise ValueError("base_delay must be >= 0")
417
+ if self.cap_delay < 0:
418
+ raise ValueError("cap_delay must be >= 0")
419
+ if self.cap_delay < self.base_delay:
420
+ raise ValueError("cap_delay must be >= base_delay")
421
+ if self.markers:
422
+ norm = tuple(m.strip().lower() for m in self.markers if m and m.strip())
423
+ object.__setattr__(self, "markers", norm or None)
424
+
425
+ def matches(self, code: int, message: t.Any) -> bool:
426
+ if self.codes is not None and code not in self.codes:
427
+ return False
428
+
429
+ if self.markers:
430
+ msg = str(message or "").lower()
431
+ if not any(m in msg for m in self.markers):
432
+ return False
433
+
434
+ return True
435
+
436
+ def delay(self, attempt_index: int) -> float:
437
+ if attempt_index < 0:
438
+ raise ValueError("attempt_index must be >= 0")
439
+ d = self.base_delay * (2**attempt_index)
440
+ return d if d < self.cap_delay else self.cap_delay
441
+
442
+
443
+ @dataclass(slots=True, frozen=True)
444
+ class RetryPolicy:
445
+ """Ordered collection of retry rules (first match wins)."""
446
+
447
+ rules: t.Tuple[RetryRule, ...]
448
+
449
+ def rule_for(self, code: int, message: t.Any) -> t.Optional[RetryRule]:
450
+ for r in self.rules:
451
+ if r.matches(code, message):
452
+ return r
453
+ return None
454
+
455
+
456
+ DEFAULT_HTTP_RETRY_POLICY = RetryPolicy(
457
+ rules=(
458
+ # rate limit exceed
459
+ RetryRule(
460
+ codes=(429,),
461
+ attempts=3,
462
+ base_delay=0.3,
463
+ cap_delay=3.0,
464
+ ),
465
+ # transient gateway/service failures
466
+ RetryRule(
467
+ codes=(502, 503, 504),
468
+ attempts=3,
469
+ base_delay=0.5,
470
+ cap_delay=5.0,
471
+ ),
472
+ # CDN/protection/challenge pages (сloudflare, etc.)
473
+ RetryRule(
474
+ attempts=3,
475
+ base_delay=1.0,
476
+ cap_delay=8.0,
477
+ markers=tuple(CDN_CHALLENGE_MARKERS.keys()),
478
+ ),
479
+ )
480
+ )
481
+ """Default retry policy for HTTP queries."""
482
+
483
+ DEFAULT_ADNL_RETRY_POLICY = RetryPolicy(
484
+ rules=(
485
+ # rate limit exceed
486
+ RetryRule(codes=(228, 5556), attempts=3),
487
+ # block (...) is not in db
488
+ RetryRule(codes=(651,), attempts=4),
489
+ # backend node timeout
490
+ RetryRule(codes=(502,), attempts=5),
491
+ )
492
+ )
493
+ """Default retry policy for ADNL queries."""
494
+
378
495
  DEFAULT_SUBWALLET_ID = 698983191
379
496
  """Default subwallet ID for wallet contracts."""
380
497
 
381
498
  DEFAULT_SENDMODE = SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS
382
499
  """Default send mode: pay fees separately and ignore errors."""
500
+
501
+ MASTERCHAIN_SHARD = -9223372036854775808
502
+ """Shard identifier for the masterchain (-2^63)."""
503
+
504
+ MAINNET_GENESIS_UTIME = 1573822385
505
+ """Unix timestamp of the TON mainnet genesis block (November 15, 2019)."""
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.1b4
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,8 @@ 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
- Requires-Dist: pycryptodomex~=3.23.0
31
29
  Requires-Dist: pydantic<3.0,>=2.0
32
- Requires-Dist: pynacl~=1.6.0
33
- Requires-Dist: pytoniq-core~=0.1.45
30
+ Requires-Dist: pytoniq-core~=0.1.46
34
31
  Dynamic: license-file
35
32
 
36
33
  # 📦 Tonutils 2.0 [BETA]