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.
- tonutils/__init__.py +0 -2
- tonutils/__meta__.py +1 -1
- tonutils/cli.py +111 -0
- tonutils/clients/__init__.py +7 -11
- tonutils/clients/adnl/__init__.py +7 -3
- tonutils/clients/adnl/balancer.py +362 -168
- tonutils/clients/adnl/client.py +203 -67
- tonutils/clients/adnl/provider/config.py +24 -25
- tonutils/clients/adnl/provider/models.py +4 -0
- tonutils/clients/adnl/provider/provider.py +203 -160
- tonutils/clients/adnl/provider/transport.py +44 -33
- 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 +146 -228
- tonutils/tonconnect/__init__.py +0 -0
- tonutils/tools/__init__.py +6 -0
- tonutils/tools/block_scanner/__init__.py +26 -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 +315 -0
- tonutils/tools/block_scanner/traversal.py +96 -0
- tonutils/tools/block_scanner/where.py +151 -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 +125 -2
- tonutils/utils.py +3 -3
- {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b4.dist-info}/METADATA +2 -5
- tonutils-2.0.1b4.dist-info/RECORD +108 -0
- tonutils-2.0.1b4.dist-info/entry_points.txt +2 -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.1b4.dist-info}/WHEEL +0 -0
- {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b4.dist-info}/licenses/LICENSE +0 -0
- {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.
|
|
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.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:
|
|
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]
|