tonutils 2.0.1b5__py3-none-any.whl → 2.0.1b7__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/clients/__init__.py +10 -10
- tonutils/clients/adnl/balancer.py +135 -361
- tonutils/clients/adnl/client.py +35 -208
- tonutils/clients/adnl/mixin.py +268 -0
- tonutils/clients/adnl/provider/config.py +22 -7
- tonutils/clients/adnl/provider/provider.py +61 -16
- tonutils/clients/adnl/provider/transport.py +13 -4
- tonutils/clients/adnl/provider/workers/pinger.py +1 -1
- tonutils/clients/adnl/utils.py +5 -5
- tonutils/clients/base.py +61 -95
- tonutils/clients/http/__init__.py +11 -11
- tonutils/clients/http/balancer.py +103 -100
- tonutils/clients/http/clients/__init__.py +10 -10
- tonutils/clients/http/clients/chainstack.py +3 -3
- tonutils/clients/http/clients/quicknode.py +2 -3
- tonutils/clients/http/clients/tatum.py +4 -3
- tonutils/clients/http/clients/tonapi.py +20 -33
- tonutils/clients/http/clients/toncenter.py +64 -55
- tonutils/clients/http/{providers → provider}/__init__.py +4 -1
- tonutils/clients/http/{providers → provider}/base.py +140 -61
- tonutils/clients/http/{providers/toncenter → provider}/models.py +44 -2
- tonutils/clients/http/{providers/tonapi/provider.py → provider/tonapi.py} +8 -13
- tonutils/clients/http/{providers/toncenter/provider.py → provider/toncenter.py} +25 -21
- tonutils/clients/limiter.py +61 -59
- tonutils/clients/protocol.py +8 -8
- tonutils/contracts/base.py +32 -32
- tonutils/contracts/protocol.py +9 -9
- tonutils/contracts/wallet/base.py +7 -8
- tonutils/contracts/wallet/messages.py +4 -8
- tonutils/contracts/wallet/versions/v5.py +2 -2
- tonutils/exceptions.py +29 -13
- tonutils/tonconnect/bridge/__init__.py +0 -0
- tonutils/tonconnect/events.py +0 -0
- tonutils/tonconnect/models/__init__.py +0 -0
- tonutils/tonconnect/storage.py +0 -0
- tonutils/tonconnect/tonconnect.py +0 -0
- tonutils/tools/block_scanner/__init__.py +2 -5
- tonutils/tools/block_scanner/events.py +48 -7
- tonutils/tools/block_scanner/scanner.py +316 -222
- tonutils/tools/block_scanner/storage.py +11 -0
- tonutils/tools/status_monitor/monitor.py +6 -6
- tonutils/types.py +2 -2
- tonutils/utils.py +0 -48
- {tonutils-2.0.1b5.dist-info → tonutils-2.0.1b7.dist-info}/METADATA +3 -18
- {tonutils-2.0.1b5.dist-info → tonutils-2.0.1b7.dist-info}/RECORD +50 -51
- {tonutils-2.0.1b5.dist-info → tonutils-2.0.1b7.dist-info}/WHEEL +1 -1
- tonutils/clients/http/providers/response.py +0 -85
- tonutils/clients/http/providers/tonapi/__init__.py +0 -3
- tonutils/clients/http/providers/tonapi/models.py +0 -47
- tonutils/clients/http/providers/toncenter/__init__.py +0 -3
- tonutils/tools/block_scanner/annotations.py +0 -23
- tonutils/tools/block_scanner/dispatcher.py +0 -141
- tonutils/tools/block_scanner/traversal.py +0 -97
- tonutils/tools/block_scanner/where.py +0 -53
- {tonutils-2.0.1b5.dist-info → tonutils-2.0.1b7.dist-info}/entry_points.txt +0 -0
- {tonutils-2.0.1b5.dist-info → tonutils-2.0.1b7.dist-info}/licenses/LICENSE +0 -0
- {tonutils-2.0.1b5.dist-info → tonutils-2.0.1b7.dist-info}/top_level.txt +0 -0
tonutils/clients/limiter.py
CHANGED
|
@@ -4,11 +4,10 @@ import time
|
|
|
4
4
|
|
|
5
5
|
class RateLimiter:
|
|
6
6
|
"""
|
|
7
|
-
Asynchronous token-bucket rate limiter with
|
|
7
|
+
Asynchronous token-bucket rate limiter with priority support.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
become available.
|
|
9
|
+
Priority requests are served before non-priority requests when
|
|
10
|
+
tokens become available.
|
|
12
11
|
"""
|
|
13
12
|
|
|
14
13
|
__slots__ = (
|
|
@@ -26,7 +25,7 @@ class RateLimiter:
|
|
|
26
25
|
|
|
27
26
|
:param max_rate: Maximum number of acquisitions allowed per period.
|
|
28
27
|
:param period: Period length in seconds.
|
|
29
|
-
:raises ValueError: If
|
|
28
|
+
:raises ValueError: If max_rate or period is not positive.
|
|
30
29
|
"""
|
|
31
30
|
if max_rate <= 0:
|
|
32
31
|
raise ValueError("max_rate must be > 0")
|
|
@@ -40,76 +39,79 @@ class RateLimiter:
|
|
|
40
39
|
self._cond = asyncio.Condition()
|
|
41
40
|
self._priority_waiters = 0
|
|
42
41
|
|
|
43
|
-
def
|
|
44
|
-
"""
|
|
45
|
-
Calculate delay until the next token becomes available.
|
|
46
|
-
|
|
47
|
-
:return: Number of seconds to wait before a token can be acquired,
|
|
48
|
-
or ``0`` if a token is available immediately.
|
|
49
|
-
"""
|
|
42
|
+
def _refill(self) -> None:
|
|
43
|
+
"""Refill tokens based on elapsed time."""
|
|
50
44
|
now = time.monotonic()
|
|
51
|
-
|
|
52
|
-
if
|
|
45
|
+
elapsed = now - self._updated_at
|
|
46
|
+
if elapsed > 0:
|
|
47
|
+
rate = self._max_rate / self._period
|
|
48
|
+
self._tokens = min(float(self._max_rate), self._tokens + elapsed * rate)
|
|
49
|
+
self._updated_at = now
|
|
50
|
+
|
|
51
|
+
def _seconds_to_token(self) -> float:
|
|
52
|
+
"""Calculate seconds until next token is available."""
|
|
53
|
+
if self._tokens >= 1.0:
|
|
53
54
|
return 0.0
|
|
54
|
-
|
|
55
|
+
missing = 1.0 - self._tokens
|
|
56
|
+
rate = self._max_rate / self._period
|
|
57
|
+
return missing / rate
|
|
55
58
|
|
|
56
59
|
async def acquire(self, priority: bool = False) -> None:
|
|
57
60
|
"""
|
|
58
|
-
Acquire a single token, waiting
|
|
61
|
+
Acquire a single token, waiting if necessary.
|
|
59
62
|
|
|
60
|
-
Priority
|
|
61
|
-
|
|
63
|
+
Priority requests bypass non-priority waiters when tokens
|
|
64
|
+
become available.
|
|
62
65
|
|
|
63
|
-
:param priority: Whether to acquire
|
|
66
|
+
:param priority: Whether to acquire with priority.
|
|
64
67
|
"""
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
self._priority_waiters += 1
|
|
68
|
+
async with self._cond:
|
|
69
|
+
is_waiting = False
|
|
68
70
|
|
|
69
71
|
try:
|
|
70
|
-
|
|
72
|
+
while True:
|
|
73
|
+
self._refill()
|
|
74
|
+
|
|
75
|
+
# Check if token available
|
|
76
|
+
if self._tokens >= 1.0:
|
|
77
|
+
# Priority always takes; non-priority only if no priority waiting
|
|
78
|
+
if priority or self._priority_waiters == 0:
|
|
79
|
+
self._tokens -= 1.0
|
|
80
|
+
self._cond.notify_all()
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
# Register as priority waiter only when actually waiting
|
|
84
|
+
if priority and not is_waiting:
|
|
85
|
+
self._priority_waiters += 1
|
|
86
|
+
is_waiting = True
|
|
87
|
+
|
|
88
|
+
# Wait for token
|
|
89
|
+
wait_time = self._seconds_to_token()
|
|
90
|
+
if wait_time > 0:
|
|
91
|
+
try:
|
|
92
|
+
await asyncio.wait_for(self._cond.wait(), timeout=wait_time)
|
|
93
|
+
except asyncio.TimeoutError:
|
|
94
|
+
pass # Expected — token should be ready now
|
|
95
|
+
else:
|
|
96
|
+
# Token available but blocked by priority — wait for notify
|
|
97
|
+
await self._cond.wait()
|
|
98
|
+
|
|
71
99
|
finally:
|
|
72
|
-
|
|
100
|
+
if is_waiting:
|
|
73
101
|
self._priority_waiters -= 1
|
|
74
102
|
self._cond.notify_all()
|
|
75
|
-
return
|
|
76
103
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
while True:
|
|
81
|
-
async with self._cond:
|
|
82
|
-
now = time.monotonic()
|
|
83
|
-
self._refill(now)
|
|
84
|
-
|
|
85
|
-
if self._tokens >= 1.0 and (priority or self._priority_waiters == 0):
|
|
86
|
-
self._tokens -= 1.0
|
|
87
|
-
self._cond.notify_all()
|
|
88
|
-
return
|
|
89
|
-
|
|
90
|
-
if self._tokens < 1.0:
|
|
91
|
-
timeout = self._seconds_to_one_token(self._tokens)
|
|
92
|
-
await asyncio.wait_for(self._cond.wait(), timeout=timeout)
|
|
93
|
-
else:
|
|
94
|
-
await self._cond.wait()
|
|
104
|
+
def when_ready(self) -> float:
|
|
105
|
+
"""
|
|
106
|
+
Calculate delay until the next token becomes available.
|
|
95
107
|
|
|
96
|
-
|
|
108
|
+
:return: Seconds to wait, or 0 if ready immediately.
|
|
109
|
+
"""
|
|
110
|
+
now = time.monotonic()
|
|
97
111
|
elapsed = now - self._updated_at
|
|
98
|
-
if elapsed <= 0.0:
|
|
99
|
-
return self._tokens
|
|
100
|
-
|
|
101
112
|
rate = self._max_rate / self._period
|
|
102
|
-
tokens = self._tokens + elapsed * rate
|
|
103
|
-
return min(float(self._max_rate), tokens)
|
|
104
|
-
|
|
105
|
-
def _refill(self, now: float) -> None:
|
|
106
|
-
self._tokens = self._peek_tokens(now)
|
|
107
|
-
self._updated_at = now
|
|
113
|
+
tokens = min(float(self._max_rate), self._tokens + elapsed * rate)
|
|
108
114
|
|
|
109
|
-
|
|
110
|
-
missing = 1.0 - tokens
|
|
111
|
-
if missing <= 0.0:
|
|
115
|
+
if tokens >= 1.0:
|
|
112
116
|
return 0.0
|
|
113
|
-
|
|
114
|
-
rate = self._max_rate / self._period
|
|
115
|
-
return missing / rate
|
|
117
|
+
return (1.0 - tokens) / rate
|
tonutils/clients/protocol.py
CHANGED
|
@@ -6,7 +6,7 @@ from pytoniq_core import Cell, Transaction
|
|
|
6
6
|
|
|
7
7
|
from tonutils.types import (
|
|
8
8
|
AddressLike,
|
|
9
|
-
|
|
9
|
+
ContractInfo,
|
|
10
10
|
DNSCategory,
|
|
11
11
|
NetworkGlobalID,
|
|
12
12
|
ClientType,
|
|
@@ -40,27 +40,27 @@ class ClientProtocol(t.Protocol):
|
|
|
40
40
|
"""Underlying provider or transport backend."""
|
|
41
41
|
|
|
42
42
|
@property
|
|
43
|
-
def
|
|
43
|
+
def connected(self) -> bool:
|
|
44
44
|
"""Whether the client is connected and ready for requests."""
|
|
45
45
|
|
|
46
|
-
async def
|
|
46
|
+
async def send_message(self, boc: str) -> None:
|
|
47
47
|
"""Send an external message to the blockchain."""
|
|
48
48
|
|
|
49
|
-
async def
|
|
49
|
+
async def get_config(self) -> t.Dict[int, t.Any]:
|
|
50
50
|
"""Fetch global blockchain configuration."""
|
|
51
51
|
|
|
52
|
-
async def
|
|
52
|
+
async def get_info(
|
|
53
53
|
self,
|
|
54
54
|
address: AddressLike,
|
|
55
|
-
) ->
|
|
55
|
+
) -> ContractInfo:
|
|
56
56
|
"""Fetch basic contract state information."""
|
|
57
57
|
|
|
58
|
-
async def
|
|
58
|
+
async def get_transactions(
|
|
59
59
|
self,
|
|
60
60
|
address: AddressLike,
|
|
61
61
|
limit: int = 100,
|
|
62
62
|
from_lt: t.Optional[int] = None,
|
|
63
|
-
to_lt: int =
|
|
63
|
+
to_lt: t.Optional[int] = None,
|
|
64
64
|
) -> t.List[Transaction]:
|
|
65
65
|
"""Fetch contract transactions."""
|
|
66
66
|
|
tonutils/contracts/base.py
CHANGED
|
@@ -13,7 +13,7 @@ from tonutils.exceptions import (
|
|
|
13
13
|
StateNotLoadedError,
|
|
14
14
|
ProviderResponseError,
|
|
15
15
|
)
|
|
16
|
-
from tonutils.types import AddressLike,
|
|
16
|
+
from tonutils.types import AddressLike, ContractInfo, ContractState, WorkchainID
|
|
17
17
|
from tonutils.utils import to_cell
|
|
18
18
|
|
|
19
19
|
_R = t.TypeVar("_R")
|
|
@@ -61,7 +61,7 @@ class BaseContract(ContractProtocol[_D]):
|
|
|
61
61
|
client: ClientProtocol,
|
|
62
62
|
address: Address,
|
|
63
63
|
state_init: t.Optional[StateInit] = None,
|
|
64
|
-
|
|
64
|
+
info: t.Optional[ContractInfo] = None,
|
|
65
65
|
) -> None:
|
|
66
66
|
"""
|
|
67
67
|
Initialize base contract wrapper.
|
|
@@ -69,12 +69,12 @@ class BaseContract(ContractProtocol[_D]):
|
|
|
69
69
|
:param client: TON client for blockchain interactions
|
|
70
70
|
:param address: Contract address on the blockchain
|
|
71
71
|
:param state_init: Optional known StateInit (code and data)
|
|
72
|
-
:param
|
|
72
|
+
:param info: Optional preloaded on-chain contract state
|
|
73
73
|
"""
|
|
74
74
|
self._client = client
|
|
75
75
|
self._address = address
|
|
76
76
|
self._state_init = state_init
|
|
77
|
-
self.
|
|
77
|
+
self._info = info
|
|
78
78
|
|
|
79
79
|
@property
|
|
80
80
|
def client(self) -> ClientProtocol:
|
|
@@ -91,17 +91,6 @@ class BaseContract(ContractProtocol[_D]):
|
|
|
91
91
|
"""Locally known StateInit for this contract, if any."""
|
|
92
92
|
return self._state_init
|
|
93
93
|
|
|
94
|
-
@property
|
|
95
|
-
def state_info(self) -> ContractStateInfo:
|
|
96
|
-
"""
|
|
97
|
-
Cached snapshot of the contract state.
|
|
98
|
-
|
|
99
|
-
:return: Contract state information
|
|
100
|
-
"""
|
|
101
|
-
if self._state_info is None:
|
|
102
|
-
raise StateNotLoadedError(self, missing="state_info")
|
|
103
|
-
return t.cast(ContractStateInfo, self._state_info)
|
|
104
|
-
|
|
105
94
|
@property
|
|
106
95
|
def state_data(self) -> _D:
|
|
107
96
|
"""
|
|
@@ -113,11 +102,22 @@ class BaseContract(ContractProtocol[_D]):
|
|
|
113
102
|
"""
|
|
114
103
|
if not hasattr(self, "_data_model") or self._data_model is None:
|
|
115
104
|
raise ContractError(self, "No `_data_model` defined for contract class.")
|
|
116
|
-
if not (self.
|
|
105
|
+
if not (self._info and self._info.data):
|
|
117
106
|
raise StateNotLoadedError(self, missing="state_data")
|
|
118
|
-
cs = self.
|
|
107
|
+
cs = self._info.data.begin_parse()
|
|
119
108
|
return self._data_model.deserialize(cs)
|
|
120
109
|
|
|
110
|
+
@property
|
|
111
|
+
def info(self) -> ContractInfo:
|
|
112
|
+
"""
|
|
113
|
+
Cached snapshot of the contract state info.
|
|
114
|
+
|
|
115
|
+
:return: Contract state information
|
|
116
|
+
"""
|
|
117
|
+
if self._info is None:
|
|
118
|
+
raise StateNotLoadedError(self, missing="info")
|
|
119
|
+
return t.cast(ContractInfo, self._info)
|
|
120
|
+
|
|
121
121
|
@property
|
|
122
122
|
def balance(self) -> int:
|
|
123
123
|
"""
|
|
@@ -125,7 +125,7 @@ class BaseContract(ContractProtocol[_D]):
|
|
|
125
125
|
|
|
126
126
|
:return: Balance from the latest known state
|
|
127
127
|
"""
|
|
128
|
-
return self.
|
|
128
|
+
return self.info.balance
|
|
129
129
|
|
|
130
130
|
@property
|
|
131
131
|
def state(self) -> ContractState:
|
|
@@ -134,7 +134,7 @@ class BaseContract(ContractProtocol[_D]):
|
|
|
134
134
|
|
|
135
135
|
:return: One of ContractState enum values (ACTIVE, FROZEN, UNINIT, NONEXIST)
|
|
136
136
|
"""
|
|
137
|
-
return self.
|
|
137
|
+
return self.info.state
|
|
138
138
|
|
|
139
139
|
@property
|
|
140
140
|
def is_active(self) -> bool:
|
|
@@ -179,7 +179,7 @@ class BaseContract(ContractProtocol[_D]):
|
|
|
179
179
|
|
|
180
180
|
:return: Transaction LT or None if unknown
|
|
181
181
|
"""
|
|
182
|
-
return self.
|
|
182
|
+
return self.info.last_transaction_lt
|
|
183
183
|
|
|
184
184
|
@property
|
|
185
185
|
def last_transaction_hash(self) -> t.Optional[str]:
|
|
@@ -188,7 +188,7 @@ class BaseContract(ContractProtocol[_D]):
|
|
|
188
188
|
|
|
189
189
|
:return: Transaction hash as hex string or None if unknown
|
|
190
190
|
"""
|
|
191
|
-
return self.
|
|
191
|
+
return self.info.last_transaction_hash
|
|
192
192
|
|
|
193
193
|
@property
|
|
194
194
|
def code(self) -> t.Optional[Cell]:
|
|
@@ -197,7 +197,7 @@ class BaseContract(ContractProtocol[_D]):
|
|
|
197
197
|
|
|
198
198
|
:return: Code cell or None if not available
|
|
199
199
|
"""
|
|
200
|
-
return self.
|
|
200
|
+
return self.info.code
|
|
201
201
|
|
|
202
202
|
@property
|
|
203
203
|
def data(self) -> t.Optional[Cell]:
|
|
@@ -206,36 +206,36 @@ class BaseContract(ContractProtocol[_D]):
|
|
|
206
206
|
|
|
207
207
|
:return: Data cell or None if not available
|
|
208
208
|
"""
|
|
209
|
-
return self.
|
|
209
|
+
return self.info.data
|
|
210
210
|
|
|
211
211
|
@classmethod
|
|
212
|
-
async def
|
|
212
|
+
async def _load_info(
|
|
213
213
|
cls,
|
|
214
214
|
client: ClientProtocol,
|
|
215
215
|
address: Address,
|
|
216
|
-
) ->
|
|
216
|
+
) -> ContractInfo:
|
|
217
217
|
"""
|
|
218
218
|
Fetch contract state from the blockchain.
|
|
219
219
|
|
|
220
220
|
If the request fails (except rate limits), sets state to default empty state.
|
|
221
221
|
"""
|
|
222
222
|
try:
|
|
223
|
-
return await client.
|
|
223
|
+
return await client.get_info(address)
|
|
224
224
|
except ProviderResponseError as e:
|
|
225
225
|
if e.code in {429, 228, 5556}: # rate limit exceed
|
|
226
226
|
raise
|
|
227
|
-
return
|
|
227
|
+
return ContractInfo()
|
|
228
228
|
except (Exception,):
|
|
229
|
-
return
|
|
229
|
+
return ContractInfo()
|
|
230
230
|
|
|
231
231
|
async def refresh(self) -> None:
|
|
232
232
|
"""
|
|
233
233
|
Refresh contract state from the blockchain.
|
|
234
234
|
|
|
235
|
-
Fetches current contract information and updates the cached
|
|
235
|
+
Fetches current contract information and updates the cached info.
|
|
236
236
|
If the request fails (except rate limits), sets state to default empty state.
|
|
237
237
|
"""
|
|
238
|
-
self.
|
|
238
|
+
self._info = await self._load_info(self.client, self.address)
|
|
239
239
|
|
|
240
240
|
@classmethod
|
|
241
241
|
def from_state_init(
|
|
@@ -323,8 +323,8 @@ class BaseContract(ContractProtocol[_D]):
|
|
|
323
323
|
if not load_state:
|
|
324
324
|
return cls(client, address)
|
|
325
325
|
|
|
326
|
-
|
|
327
|
-
return cls(client, address,
|
|
326
|
+
info = await cls._load_info(client, address)
|
|
327
|
+
return cls(client, address, info=info)
|
|
328
328
|
|
|
329
329
|
def __repr__(self) -> str:
|
|
330
330
|
return f"< Contract {self.__class__.__name__} address: {self.address} >"
|
tonutils/contracts/protocol.py
CHANGED
|
@@ -5,7 +5,7 @@ import typing as t
|
|
|
5
5
|
from pytoniq_core import Address, Cell, StateInit
|
|
6
6
|
|
|
7
7
|
from tonutils.clients.protocol import ClientProtocol
|
|
8
|
-
from tonutils.types import AddressLike,
|
|
8
|
+
from tonutils.types import AddressLike, ContractInfo, ContractState, WorkchainID
|
|
9
9
|
|
|
10
10
|
if t.TYPE_CHECKING:
|
|
11
11
|
from tonutils.contracts.versions import ContractVersion
|
|
@@ -48,21 +48,21 @@ class ContractProtocol(t.Protocol[_D]):
|
|
|
48
48
|
"""Locally known StateInit for this contract, if any."""
|
|
49
49
|
|
|
50
50
|
@property
|
|
51
|
-
def
|
|
51
|
+
def state_data(self) -> _D:
|
|
52
52
|
"""
|
|
53
|
-
|
|
53
|
+
Decoded on-chain data in typed form.
|
|
54
54
|
|
|
55
|
-
Implementations
|
|
56
|
-
|
|
55
|
+
Implementations must decode the current data cell from info
|
|
56
|
+
using the associated _data_model.
|
|
57
57
|
"""
|
|
58
58
|
|
|
59
59
|
@property
|
|
60
|
-
def
|
|
60
|
+
def info(self) -> ContractInfo:
|
|
61
61
|
"""
|
|
62
|
-
|
|
62
|
+
Cached snapshot of the contract state info.
|
|
63
63
|
|
|
64
|
-
Implementations
|
|
65
|
-
|
|
64
|
+
Implementations usually update this via refresh() or in constructors
|
|
65
|
+
that read on-chain data.
|
|
66
66
|
"""
|
|
67
67
|
|
|
68
68
|
@property
|
|
@@ -22,14 +22,14 @@ from tonutils.contracts.wallet.tlb import BaseWalletData
|
|
|
22
22
|
from tonutils.exceptions import ContractError
|
|
23
23
|
from tonutils.types import (
|
|
24
24
|
AddressLike,
|
|
25
|
-
|
|
25
|
+
ContractInfo,
|
|
26
26
|
SendMode,
|
|
27
27
|
PublicKey,
|
|
28
28
|
PrivateKey,
|
|
29
29
|
WorkchainID,
|
|
30
30
|
DEFAULT_SENDMODE,
|
|
31
31
|
)
|
|
32
|
-
from tonutils.utils import
|
|
32
|
+
from tonutils.utils import to_cell
|
|
33
33
|
|
|
34
34
|
_D = t.TypeVar("_D", bound=BaseWalletData)
|
|
35
35
|
_C = t.TypeVar("_C", bound=BaseWalletConfig)
|
|
@@ -61,7 +61,7 @@ class BaseWallet(BaseContract, WalletProtocol[_D, _C, _P], abc.ABC):
|
|
|
61
61
|
client: ClientProtocol,
|
|
62
62
|
address: Address,
|
|
63
63
|
state_init: t.Optional[StateInit] = None,
|
|
64
|
-
|
|
64
|
+
info: t.Optional[ContractInfo] = None,
|
|
65
65
|
config: t.Optional[_C] = None,
|
|
66
66
|
private_key: t.Optional[PrivateKey] = None,
|
|
67
67
|
) -> None:
|
|
@@ -71,7 +71,7 @@ class BaseWallet(BaseContract, WalletProtocol[_D, _C, _P], abc.ABC):
|
|
|
71
71
|
:param client: TON client for blockchain interactions
|
|
72
72
|
:param address: Wallet address on the blockchain
|
|
73
73
|
:param state_init: Optional StateInit (code and data)
|
|
74
|
-
:param
|
|
74
|
+
:param info: Optional preloaded contract state
|
|
75
75
|
:param config: Optional wallet configuration
|
|
76
76
|
:param private_key: Optional private key for signing transactions
|
|
77
77
|
"""
|
|
@@ -82,7 +82,7 @@ class BaseWallet(BaseContract, WalletProtocol[_D, _C, _P], abc.ABC):
|
|
|
82
82
|
if private_key is not None:
|
|
83
83
|
self._private_key = private_key
|
|
84
84
|
self._public_key = private_key.public_key
|
|
85
|
-
super().__init__(client, address, state_init,
|
|
85
|
+
super().__init__(client, address, state_init, info)
|
|
86
86
|
|
|
87
87
|
@property
|
|
88
88
|
def state_data(self) -> _D:
|
|
@@ -295,7 +295,7 @@ class BaseWallet(BaseContract, WalletProtocol[_D, _C, _P], abc.ABC):
|
|
|
295
295
|
:return: Signed external message that was sent
|
|
296
296
|
"""
|
|
297
297
|
external_msg = await self.build_external_message(messages, params)
|
|
298
|
-
await self.client.
|
|
298
|
+
await self.client.send_message(external_msg.as_hex)
|
|
299
299
|
return external_msg
|
|
300
300
|
|
|
301
301
|
async def transfer_message(
|
|
@@ -325,7 +325,7 @@ class BaseWallet(BaseContract, WalletProtocol[_D, _C, _P], abc.ABC):
|
|
|
325
325
|
"""
|
|
326
326
|
Build and send a transfer to a single destination.
|
|
327
327
|
|
|
328
|
-
:param destination: Recipient address
|
|
328
|
+
:param destination: Recipient address
|
|
329
329
|
:param amount: Amount to send in nanotons
|
|
330
330
|
:param body: Optional message body (Cell or text comment)
|
|
331
331
|
:param state_init: Optional StateInit for contract deployment
|
|
@@ -334,7 +334,6 @@ class BaseWallet(BaseContract, WalletProtocol[_D, _C, _P], abc.ABC):
|
|
|
334
334
|
:param params: Optional transaction parameters
|
|
335
335
|
:return: Signed external message that was sent
|
|
336
336
|
"""
|
|
337
|
-
destination = await resolve_wallet_address(self.client, destination)
|
|
338
337
|
message = TONTransferBuilder(
|
|
339
338
|
destination=destination,
|
|
340
339
|
amount=amount,
|
|
@@ -23,7 +23,6 @@ from tonutils.utils import (
|
|
|
23
23
|
cell_to_b64,
|
|
24
24
|
normalize_hash,
|
|
25
25
|
to_nano,
|
|
26
|
-
resolve_wallet_address,
|
|
27
26
|
)
|
|
28
27
|
|
|
29
28
|
|
|
@@ -191,7 +190,7 @@ class TONTransferBuilder(BaseMessageBuilder):
|
|
|
191
190
|
"""
|
|
192
191
|
Initialize TON transfer builder.
|
|
193
192
|
|
|
194
|
-
:param destination: Recipient address
|
|
193
|
+
:param destination: Recipient address
|
|
195
194
|
:param amount: Amount to send in nanotons
|
|
196
195
|
:param body: Optional message body (Cell or text comment string)
|
|
197
196
|
:param state_init: Optional StateInit for contract deployment
|
|
@@ -214,11 +213,10 @@ class TONTransferBuilder(BaseMessageBuilder):
|
|
|
214
213
|
:param wallet: Wallet instance to build message for
|
|
215
214
|
:return: WalletMessage with TON transfer
|
|
216
215
|
"""
|
|
217
|
-
destination = await resolve_wallet_address(wallet.client, self.destination)
|
|
218
216
|
return WalletMessage(
|
|
219
217
|
send_mode=self.send_mode,
|
|
220
218
|
message=InternalMessage(
|
|
221
|
-
dest=destination,
|
|
219
|
+
dest=self.destination,
|
|
222
220
|
value=self.amount,
|
|
223
221
|
body=self.body,
|
|
224
222
|
state_init=self.state_init,
|
|
@@ -277,9 +275,8 @@ class NFTTransferBuilder(BaseMessageBuilder):
|
|
|
277
275
|
:param wallet: Wallet instance to build message for
|
|
278
276
|
:return: WalletMessage with NFT transfer
|
|
279
277
|
"""
|
|
280
|
-
destination = await resolve_wallet_address(wallet.client, self.destination)
|
|
281
278
|
body = NFTTransferBody(
|
|
282
|
-
destination=destination,
|
|
279
|
+
destination=self.destination,
|
|
283
280
|
response_address=self.response_address or wallet.address,
|
|
284
281
|
custom_payload=self.custom_payload,
|
|
285
282
|
forward_payload=self.forward_payload,
|
|
@@ -358,7 +355,6 @@ class JettonTransferBuilder(BaseMessageBuilder):
|
|
|
358
355
|
:param wallet: Wallet instance to build message for
|
|
359
356
|
:return: WalletMessage with jetton transfer
|
|
360
357
|
"""
|
|
361
|
-
destination = await resolve_wallet_address(wallet.client, self.destination)
|
|
362
358
|
jetton_wallet_address = self.jetton_wallet_address
|
|
363
359
|
if self.jetton_wallet_address is None:
|
|
364
360
|
jetton_wallet_address = await get_wallet_address_get_method(
|
|
@@ -367,7 +363,7 @@ class JettonTransferBuilder(BaseMessageBuilder):
|
|
|
367
363
|
owner_address=wallet.address,
|
|
368
364
|
)
|
|
369
365
|
body = JettonTransferBody(
|
|
370
|
-
destination=destination,
|
|
366
|
+
destination=self.destination,
|
|
371
367
|
jetton_amount=self.jetton_amount,
|
|
372
368
|
response_address=self.response_address or wallet.address,
|
|
373
369
|
custom_payload=self.custom_payload,
|
|
@@ -219,13 +219,13 @@ class WalletV5R1(
|
|
|
219
219
|
|
|
220
220
|
:return: Typed wallet data
|
|
221
221
|
"""
|
|
222
|
-
if not (self.
|
|
222
|
+
if not (self._info and self._info.data):
|
|
223
223
|
raise StateNotLoadedError(self, missing="state_data")
|
|
224
224
|
|
|
225
225
|
network_global_id = (
|
|
226
226
|
NetworkGlobalID.TESTNET if self.client.network else NetworkGlobalID.MAINNET
|
|
227
227
|
)
|
|
228
|
-
cs = self.
|
|
228
|
+
cs = self._info.data.begin_parse()
|
|
229
229
|
return self._data_model.deserialize(cs, network_global_id)
|
|
230
230
|
|
|
231
231
|
async def _build_msg_cell(
|
tonutils/exceptions.py
CHANGED
|
@@ -27,7 +27,7 @@ class TransportError(TonutilsError):
|
|
|
27
27
|
|
|
28
28
|
Covers: TCP connect, ADNL handshake, send/recv, crypto failures.
|
|
29
29
|
|
|
30
|
-
:param endpoint:
|
|
30
|
+
:param endpoint: Endpoint identifier (URL or "host:port").
|
|
31
31
|
:param operation: What was attempted ("connect", "handshake", "send", "recv")
|
|
32
32
|
:param reason: Why it failed ("timeout 2.0s", "connection refused", etc.)
|
|
33
33
|
"""
|
|
@@ -46,7 +46,7 @@ class TransportError(TonutilsError):
|
|
|
46
46
|
self.endpoint = endpoint
|
|
47
47
|
self.operation = operation
|
|
48
48
|
self.reason = reason
|
|
49
|
-
super().__init__(f"{operation} failed
|
|
49
|
+
super().__init__(f"{operation} failed: {reason} ({endpoint})")
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
class ProviderError(TonutilsError):
|
|
@@ -64,14 +64,28 @@ class BalancerError(TonutilsError):
|
|
|
64
64
|
class NotConnectedError(TonutilsError, RuntimeError):
|
|
65
65
|
"""Raise when an operation requires an active connection."""
|
|
66
66
|
|
|
67
|
+
component: str
|
|
67
68
|
endpoint: t.Optional[str]
|
|
69
|
+
operation: t.Optional[str]
|
|
68
70
|
|
|
69
|
-
def __init__(
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
*,
|
|
74
|
+
component: str = "client",
|
|
75
|
+
endpoint: t.Optional[str] = None,
|
|
76
|
+
operation: t.Optional[str] = None,
|
|
77
|
+
hint: t.Optional[str] = None,
|
|
78
|
+
) -> None:
|
|
79
|
+
self.component = component
|
|
70
80
|
self.endpoint = endpoint
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
self.operation = operation
|
|
82
|
+
|
|
83
|
+
if hint is None:
|
|
84
|
+
hint = "Call connect() first or use an async context manager (`async with ...`)."
|
|
85
|
+
|
|
86
|
+
where = f" ({endpoint})" if endpoint else ""
|
|
87
|
+
prefix = f"cannot `{operation}`: " if operation else ""
|
|
88
|
+
super().__init__(f"{prefix}{component} is not connected{where}. {hint}")
|
|
75
89
|
|
|
76
90
|
|
|
77
91
|
class ProviderTimeoutError(ProviderError, asyncio.TimeoutError):
|
|
@@ -90,7 +104,7 @@ class ProviderTimeoutError(ProviderError, asyncio.TimeoutError):
|
|
|
90
104
|
self.timeout = timeout
|
|
91
105
|
self.endpoint = endpoint
|
|
92
106
|
self.operation = operation
|
|
93
|
-
super().__init__(f"{operation} timed out after {timeout}s
|
|
107
|
+
super().__init__(f"{operation} timed out after {timeout}s ({endpoint})")
|
|
94
108
|
|
|
95
109
|
|
|
96
110
|
class ProviderResponseError(ProviderError):
|
|
@@ -109,7 +123,7 @@ class ProviderResponseError(ProviderError):
|
|
|
109
123
|
self.code = code
|
|
110
124
|
self.message = message
|
|
111
125
|
self.endpoint = endpoint
|
|
112
|
-
super().__init__(f"request failed
|
|
126
|
+
super().__init__(f"request failed: {code} {message} ({endpoint})")
|
|
113
127
|
|
|
114
128
|
|
|
115
129
|
class RetryLimitError(ProviderError):
|
|
@@ -134,7 +148,9 @@ class RetryLimitError(ProviderError):
|
|
|
134
148
|
self.attempts = attempts
|
|
135
149
|
self.max_attempts = max_attempts
|
|
136
150
|
self.last_error = last_error
|
|
137
|
-
super().__init__(
|
|
151
|
+
super().__init__(
|
|
152
|
+
f"retry limit reached ({attempts}/{max_attempts}): {last_error}"
|
|
153
|
+
)
|
|
138
154
|
|
|
139
155
|
|
|
140
156
|
class ContractError(ClientError):
|
|
@@ -153,14 +169,14 @@ class ContractError(ClientError):
|
|
|
153
169
|
name = (
|
|
154
170
|
target.__name__ if isinstance(target, type) else target.__class__.__name__
|
|
155
171
|
)
|
|
156
|
-
super().__init__(f"{name}: {message}")
|
|
172
|
+
super().__init__(f"{name} failed: {message}")
|
|
157
173
|
|
|
158
174
|
|
|
159
175
|
class StateNotLoadedError(ContractError):
|
|
160
176
|
"""Raise when a contract wrapper requires state that is not loaded.
|
|
161
177
|
|
|
162
178
|
:param contract: Contract instance related to the failure.
|
|
163
|
-
:param missing: Missing field name (e.g. "
|
|
179
|
+
:param missing: Missing field name (e.g. "info", "state_data").
|
|
164
180
|
"""
|
|
165
181
|
|
|
166
182
|
missing: str
|
|
@@ -188,7 +204,7 @@ class RunGetMethodError(ClientError):
|
|
|
188
204
|
self.method_name = method_name
|
|
189
205
|
self.exit_code = exit_code
|
|
190
206
|
super().__init__(
|
|
191
|
-
f"get-method
|
|
207
|
+
f"get-method `{method_name}` failed: exit code {exit_code} ({address})"
|
|
192
208
|
)
|
|
193
209
|
|
|
194
210
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|