tonutils 2.0.1b2__py3-none-any.whl → 2.0.1b3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tonutils/__init__.py +0 -2
- tonutils/__meta__.py +1 -1
- tonutils/clients/__init__.py +5 -9
- tonutils/clients/adnl/__init__.py +5 -1
- tonutils/clients/adnl/balancer.py +319 -125
- tonutils/clients/adnl/client.py +187 -51
- tonutils/clients/adnl/provider/config.py +19 -25
- tonutils/clients/adnl/provider/models.py +4 -0
- tonutils/clients/adnl/provider/provider.py +191 -145
- tonutils/clients/adnl/provider/transport.py +38 -32
- tonutils/clients/adnl/provider/workers/base.py +0 -2
- tonutils/clients/adnl/provider/workers/pinger.py +1 -1
- tonutils/clients/adnl/provider/workers/reader.py +3 -2
- tonutils/clients/adnl/{provider/builder.py → utils.py} +62 -2
- tonutils/clients/http/__init__.py +11 -8
- tonutils/clients/http/balancer.py +75 -63
- tonutils/clients/http/clients/__init__.py +13 -0
- tonutils/clients/http/clients/chainstack.py +48 -0
- tonutils/clients/http/clients/quicknode.py +47 -0
- tonutils/clients/http/clients/tatum.py +56 -0
- tonutils/clients/http/{tonapi/client.py → clients/tonapi.py} +31 -31
- tonutils/clients/http/{toncenter/client.py → clients/toncenter.py} +59 -48
- tonutils/clients/http/providers/__init__.py +4 -0
- tonutils/clients/http/providers/base.py +201 -0
- tonutils/clients/http/providers/response.py +85 -0
- tonutils/clients/http/providers/tonapi/__init__.py +3 -0
- tonutils/clients/http/{tonapi → providers/tonapi}/models.py +1 -0
- tonutils/clients/http/providers/tonapi/provider.py +125 -0
- tonutils/clients/http/providers/toncenter/__init__.py +3 -0
- tonutils/clients/http/{toncenter → providers/toncenter}/models.py +1 -0
- tonutils/clients/http/providers/toncenter/provider.py +119 -0
- tonutils/clients/http/utils.py +140 -0
- tonutils/clients/limiter.py +115 -0
- tonutils/contracts/__init__.py +4 -0
- tonutils/contracts/base.py +33 -20
- tonutils/contracts/dns/methods.py +2 -2
- tonutils/contracts/jetton/methods.py +2 -2
- tonutils/contracts/nft/methods.py +2 -2
- tonutils/contracts/nft/tlb.py +1 -1
- tonutils/{protocols/contract.py → contracts/protocol.py} +29 -29
- tonutils/contracts/telegram/methods.py +2 -2
- tonutils/contracts/vanity/vanity.py +1 -1
- tonutils/contracts/wallet/__init__.py +2 -0
- tonutils/contracts/wallet/base.py +3 -3
- tonutils/contracts/wallet/messages.py +1 -1
- tonutils/contracts/wallet/methods.py +2 -2
- tonutils/{protocols/wallet.py → contracts/wallet/protocol.py} +35 -35
- tonutils/contracts/wallet/versions/v5.py +3 -3
- tonutils/exceptions.py +134 -226
- tonutils/types.py +115 -0
- tonutils/utils.py +3 -3
- {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b3.dist-info}/METADATA +2 -2
- tonutils-2.0.1b3.dist-info/RECORD +93 -0
- tonutils/clients/adnl/provider/limiter.py +0 -56
- tonutils/clients/adnl/stack.py +0 -64
- tonutils/clients/http/chainstack/__init__.py +0 -4
- tonutils/clients/http/chainstack/client.py +0 -63
- tonutils/clients/http/chainstack/provider.py +0 -44
- tonutils/clients/http/quicknode/__init__.py +0 -4
- tonutils/clients/http/quicknode/client.py +0 -60
- tonutils/clients/http/quicknode/provider.py +0 -42
- tonutils/clients/http/tatum/__init__.py +0 -4
- tonutils/clients/http/tatum/client.py +0 -66
- tonutils/clients/http/tatum/provider.py +0 -53
- tonutils/clients/http/tonapi/__init__.py +0 -4
- tonutils/clients/http/tonapi/provider.py +0 -150
- tonutils/clients/http/tonapi/stack.py +0 -71
- tonutils/clients/http/toncenter/__init__.py +0 -4
- tonutils/clients/http/toncenter/provider.py +0 -145
- tonutils/clients/http/toncenter/stack.py +0 -73
- tonutils/protocols/__init__.py +0 -9
- tonutils-2.0.1b2.dist-info/RECORD +0 -98
- /tonutils/{protocols/client.py → clients/protocol.py} +0 -0
- {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b3.dist-info}/WHEEL +0 -0
- {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b3.dist-info}/licenses/LICENSE +0 -0
- {tonutils-2.0.1b2.dist-info → tonutils-2.0.1b3.dist-info}/top_level.txt +0 -0
|
@@ -7,11 +7,20 @@ from aiohttp import ClientSession
|
|
|
7
7
|
from pytoniq_core import Cell, Slice, Transaction
|
|
8
8
|
|
|
9
9
|
from tonutils.clients.base import BaseClient
|
|
10
|
-
from tonutils.clients.http.toncenter.models import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
from tonutils.
|
|
10
|
+
from tonutils.clients.http.providers.toncenter.models import (
|
|
11
|
+
SendBocPayload,
|
|
12
|
+
RunGetMethodPayload,
|
|
13
|
+
)
|
|
14
|
+
from tonutils.clients.http.providers.toncenter.provider import ToncenterHttpProvider
|
|
15
|
+
from tonutils.clients.http.utils import decode_toncenter_stack, encode_toncenter_stack
|
|
16
|
+
from tonutils.exceptions import ClientError, RunGetMethodError
|
|
17
|
+
from tonutils.types import (
|
|
18
|
+
ClientType,
|
|
19
|
+
ContractState,
|
|
20
|
+
ContractStateInfo,
|
|
21
|
+
NetworkGlobalID,
|
|
22
|
+
RetryPolicy,
|
|
23
|
+
)
|
|
15
24
|
from tonutils.utils import cell_to_hex, parse_stack_config
|
|
16
25
|
|
|
17
26
|
|
|
@@ -26,11 +35,13 @@ class ToncenterHttpClient(BaseClient):
|
|
|
26
35
|
network: NetworkGlobalID = NetworkGlobalID.MAINNET,
|
|
27
36
|
api_key: t.Optional[str] = None,
|
|
28
37
|
base_url: t.Optional[str] = None,
|
|
29
|
-
timeout:
|
|
38
|
+
timeout: float = 10.0,
|
|
30
39
|
session: t.Optional[ClientSession] = None,
|
|
40
|
+
headers: t.Optional[t.Dict[str, str]] = None,
|
|
41
|
+
cookies: t.Optional[t.Dict[str, str]] = None,
|
|
31
42
|
rps_limit: t.Optional[int] = None,
|
|
32
43
|
rps_period: float = 1.0,
|
|
33
|
-
|
|
44
|
+
retry_policy: t.Optional[RetryPolicy] = None,
|
|
34
45
|
) -> None:
|
|
35
46
|
"""
|
|
36
47
|
Initialize Toncenter HTTP client.
|
|
@@ -39,11 +50,13 @@ class ToncenterHttpClient(BaseClient):
|
|
|
39
50
|
:param api_key: Optional Toncenter API key
|
|
40
51
|
You can get an API key on the Toncenter telegram bot: https://t.me/toncenter
|
|
41
52
|
:param base_url: Custom Toncenter endpoint base URL
|
|
42
|
-
:param timeout:
|
|
43
|
-
:param session: Optional
|
|
44
|
-
:param
|
|
45
|
-
:param
|
|
46
|
-
:param
|
|
53
|
+
:param timeout: Total request timeout in seconds.
|
|
54
|
+
:param session: Optional external aiohttp session.
|
|
55
|
+
:param headers: Default headers for owned session.
|
|
56
|
+
:param cookies: Default cookies for owned session.
|
|
57
|
+
:param rps_limit: Optional requests-per-period limit.
|
|
58
|
+
:param rps_period: Rate limit period in seconds.
|
|
59
|
+
:param retry_policy: Optional retry policy that defines per-error-code retry rules
|
|
47
60
|
"""
|
|
48
61
|
self.network: NetworkGlobalID = network
|
|
49
62
|
self._provider: ToncenterHttpProvider = ToncenterHttpProvider(
|
|
@@ -52,34 +65,24 @@ class ToncenterHttpClient(BaseClient):
|
|
|
52
65
|
base_url=base_url,
|
|
53
66
|
timeout=timeout,
|
|
54
67
|
session=session,
|
|
68
|
+
headers=headers,
|
|
69
|
+
cookies=cookies,
|
|
55
70
|
rps_limit=rps_limit,
|
|
56
71
|
rps_period=rps_period,
|
|
57
|
-
|
|
72
|
+
retry_policy=retry_policy,
|
|
58
73
|
)
|
|
59
74
|
|
|
60
75
|
@property
|
|
61
76
|
def provider(self) -> ToncenterHttpProvider:
|
|
62
|
-
"""
|
|
63
|
-
Underlying Toncenter HTTP provider.
|
|
64
|
-
|
|
65
|
-
:return: ToncenterHttpProvider instance used for all HTTP requests
|
|
66
|
-
"""
|
|
67
|
-
if not self.is_connected:
|
|
68
|
-
raise ClientNotConnectedError(self)
|
|
69
77
|
return self._provider
|
|
70
78
|
|
|
71
79
|
@property
|
|
72
80
|
def is_connected(self) -> bool:
|
|
73
|
-
"""
|
|
74
|
-
Check whether HTTP session is initialized and open.
|
|
75
|
-
|
|
76
|
-
:return: True if session exists and is not closed, False otherwise
|
|
77
|
-
"""
|
|
78
81
|
session = self._provider.session
|
|
79
82
|
return session is not None and not session.closed
|
|
80
83
|
|
|
81
84
|
async def __aenter__(self) -> ToncenterHttpClient:
|
|
82
|
-
await self._provider.
|
|
85
|
+
await self._provider.connect()
|
|
83
86
|
return self
|
|
84
87
|
|
|
85
88
|
async def __aexit__(
|
|
@@ -88,7 +91,7 @@ class ToncenterHttpClient(BaseClient):
|
|
|
88
91
|
exc_value: t.Optional[BaseException],
|
|
89
92
|
traceback: t.Optional[t.Any],
|
|
90
93
|
) -> None:
|
|
91
|
-
await self._provider.
|
|
94
|
+
await self._provider.close()
|
|
92
95
|
|
|
93
96
|
async def _send_boc(self, boc: str) -> None:
|
|
94
97
|
payload = SendBocPayload(boc=boc)
|
|
@@ -131,19 +134,25 @@ class ToncenterHttpClient(BaseClient):
|
|
|
131
134
|
|
|
132
135
|
last_transaction_lt = last_transaction_hash = None
|
|
133
136
|
|
|
134
|
-
|
|
137
|
+
tx_id = request.result.last_transaction_id
|
|
138
|
+
if tx_id is not None:
|
|
135
139
|
try:
|
|
136
|
-
lt = int(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
140
|
+
lt = int(tx_id.lt) if tx_id.lt is not None else 0
|
|
141
|
+
except (ValueError,):
|
|
142
|
+
pass
|
|
143
|
+
else:
|
|
144
|
+
if lt > 0:
|
|
145
|
+
last_transaction_lt = lt
|
|
146
|
+
|
|
147
|
+
raw = tx_id.hash
|
|
148
|
+
if raw is not None:
|
|
149
|
+
try:
|
|
150
|
+
h = base64.b64decode(raw).hex()
|
|
151
|
+
except (Exception,):
|
|
152
|
+
pass
|
|
153
|
+
else:
|
|
154
|
+
if h != "00" * 32:
|
|
155
|
+
last_transaction_hash = h
|
|
147
156
|
|
|
148
157
|
contract_info.last_transaction_lt = last_transaction_lt
|
|
149
158
|
contract_info.last_transaction_hash = last_transaction_hash
|
|
@@ -191,21 +200,23 @@ class ToncenterHttpClient(BaseClient):
|
|
|
191
200
|
payload = RunGetMethodPayload(
|
|
192
201
|
address=address,
|
|
193
202
|
method=method_name,
|
|
194
|
-
stack=
|
|
203
|
+
stack=encode_toncenter_stack(stack or []),
|
|
195
204
|
)
|
|
196
205
|
request = await self.provider.run_get_method(payload=payload)
|
|
197
206
|
if request.result is None:
|
|
198
207
|
return []
|
|
199
|
-
return decode_stack(request.result.stack or [])
|
|
200
208
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
209
|
+
if request.result.exit_code != 0:
|
|
210
|
+
raise RunGetMethodError(
|
|
211
|
+
address=address,
|
|
212
|
+
method_name=method_name,
|
|
213
|
+
exit_code=request.result.exit_code,
|
|
214
|
+
)
|
|
204
215
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
216
|
+
return decode_toncenter_stack(request.result.stack or [])
|
|
217
|
+
|
|
218
|
+
async def connect(self) -> None:
|
|
219
|
+
await self._provider.connect()
|
|
208
220
|
|
|
209
221
|
async def close(self) -> None:
|
|
210
|
-
"""Close HTTP session if it is owned by the provider."""
|
|
211
222
|
await self._provider.close()
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import typing as t
|
|
5
|
+
|
|
6
|
+
import aiohttp
|
|
7
|
+
|
|
8
|
+
from tonutils.clients.http.providers.response import HttpResponse
|
|
9
|
+
from tonutils.clients.limiter import RateLimiter
|
|
10
|
+
from tonutils.exceptions import (
|
|
11
|
+
NotConnectedError,
|
|
12
|
+
ProviderResponseError,
|
|
13
|
+
ProviderTimeoutError,
|
|
14
|
+
RetryLimitError,
|
|
15
|
+
)
|
|
16
|
+
from tonutils.types import RetryPolicy
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HttpProvider:
|
|
20
|
+
"""HTTP-based provider for TON HTTP APIs."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
base_url: str,
|
|
26
|
+
timeout: float = 10.0,
|
|
27
|
+
session: t.Optional[aiohttp.ClientSession] = None,
|
|
28
|
+
headers: t.Optional[t.Dict[str, str]] = None,
|
|
29
|
+
cookies: t.Optional[t.Dict[str, str]] = None,
|
|
30
|
+
rps_limit: t.Optional[int] = None,
|
|
31
|
+
rps_period: float = 1.0,
|
|
32
|
+
retry_policy: t.Optional[RetryPolicy] = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Initialize HTTP provider.
|
|
35
|
+
|
|
36
|
+
:param base_url: Base endpoint URL without trailing slash.
|
|
37
|
+
:param timeout: Total request timeout in seconds.
|
|
38
|
+
:param session: Optional external aiohttp session.
|
|
39
|
+
:param headers: Default headers for owned session.
|
|
40
|
+
:param cookies: Default cookies for owned session.
|
|
41
|
+
:param rps_limit: Optional requests-per-period limit.
|
|
42
|
+
:param rps_period: Rate limit period in seconds.
|
|
43
|
+
:param retry_policy: Optional retry policy that defines per-error-code retry rules
|
|
44
|
+
"""
|
|
45
|
+
self._base_url = base_url.rstrip("/")
|
|
46
|
+
self._timeout = timeout
|
|
47
|
+
self._headers = headers
|
|
48
|
+
self._cookies = cookies
|
|
49
|
+
|
|
50
|
+
self._session = session
|
|
51
|
+
self._owns_session = session is None
|
|
52
|
+
|
|
53
|
+
self._limiter = (
|
|
54
|
+
RateLimiter(max_rate=rps_limit, period=rps_period)
|
|
55
|
+
if rps_limit is not None
|
|
56
|
+
else None
|
|
57
|
+
)
|
|
58
|
+
self._retry_policy = retry_policy
|
|
59
|
+
self._connect_lock = asyncio.Lock()
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def session(self) -> t.Optional[aiohttp.ClientSession]:
|
|
63
|
+
"""Underlying aiohttp session, or None if not connected."""
|
|
64
|
+
return self._session
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def is_connected(self) -> bool:
|
|
68
|
+
"""Check whether the provider session is initialized and open."""
|
|
69
|
+
return self._session is not None and not self._session.closed
|
|
70
|
+
|
|
71
|
+
async def connect(self) -> None:
|
|
72
|
+
"""Initialize HTTP session if not already connected."""
|
|
73
|
+
if self.is_connected:
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
async with self._connect_lock:
|
|
77
|
+
if self.is_connected:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
self._session = aiohttp.ClientSession(
|
|
81
|
+
headers=self._headers,
|
|
82
|
+
cookies=self._cookies,
|
|
83
|
+
timeout=aiohttp.ClientTimeout(total=self._timeout),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
async def close(self) -> None:
|
|
87
|
+
"""Close owned HTTP session and release resources."""
|
|
88
|
+
async with self._connect_lock:
|
|
89
|
+
if self._owns_session and self._session and not self._session.closed:
|
|
90
|
+
await self._session.close()
|
|
91
|
+
self._session = None
|
|
92
|
+
|
|
93
|
+
async def _send_once(
|
|
94
|
+
self,
|
|
95
|
+
method: str,
|
|
96
|
+
path: str,
|
|
97
|
+
*,
|
|
98
|
+
params: t.Any = None,
|
|
99
|
+
json_data: t.Any = None,
|
|
100
|
+
) -> t.Any:
|
|
101
|
+
"""Send a single HTTP request.
|
|
102
|
+
|
|
103
|
+
Performs exactly one request attempt:
|
|
104
|
+
- applies rate limiting if configured
|
|
105
|
+
- converts network and protocol errors into provider exceptions
|
|
106
|
+
|
|
107
|
+
:param method: HTTP method (GET, POST, etc.).
|
|
108
|
+
:param path: Endpoint path relative to base_url.
|
|
109
|
+
:param params: Optional query parameters.
|
|
110
|
+
:param json_data: Optional JSON body.
|
|
111
|
+
:return: Parsed response payload.
|
|
112
|
+
"""
|
|
113
|
+
if not self.is_connected:
|
|
114
|
+
raise NotConnectedError()
|
|
115
|
+
|
|
116
|
+
assert self._session is not None
|
|
117
|
+
url = f"{self._base_url}/{path.lstrip('/')}"
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
if self._limiter:
|
|
121
|
+
await self._limiter.acquire()
|
|
122
|
+
|
|
123
|
+
async with self._session.request(
|
|
124
|
+
method=method,
|
|
125
|
+
url=url,
|
|
126
|
+
params=params,
|
|
127
|
+
json=json_data,
|
|
128
|
+
) as resp:
|
|
129
|
+
data = await HttpResponse.read(resp)
|
|
130
|
+
if resp.status >= 400:
|
|
131
|
+
HttpResponse.raise_error(
|
|
132
|
+
status=int(resp.status),
|
|
133
|
+
url=url,
|
|
134
|
+
data=data,
|
|
135
|
+
)
|
|
136
|
+
return data
|
|
137
|
+
|
|
138
|
+
except asyncio.TimeoutError as exc:
|
|
139
|
+
raise ProviderTimeoutError(
|
|
140
|
+
timeout=self._timeout,
|
|
141
|
+
endpoint=url,
|
|
142
|
+
operation="http request",
|
|
143
|
+
) from exc
|
|
144
|
+
|
|
145
|
+
except aiohttp.ClientError as exc:
|
|
146
|
+
raise ProviderResponseError(
|
|
147
|
+
code=0,
|
|
148
|
+
message=str(exc),
|
|
149
|
+
endpoint=url,
|
|
150
|
+
) from exc
|
|
151
|
+
|
|
152
|
+
async def send_http_request(
|
|
153
|
+
self,
|
|
154
|
+
method: str,
|
|
155
|
+
path: str,
|
|
156
|
+
*,
|
|
157
|
+
params: t.Any = None,
|
|
158
|
+
json_data: t.Any = None,
|
|
159
|
+
) -> t.Any:
|
|
160
|
+
"""Send an HTTP request with retry handling.
|
|
161
|
+
|
|
162
|
+
On provider error, retries the request according to the retry policy
|
|
163
|
+
matched by error code and message. If no rule matches, or retry attempts
|
|
164
|
+
are exhausted, the error is raised.
|
|
165
|
+
|
|
166
|
+
:param method: HTTP method.
|
|
167
|
+
:param path: Endpoint path relative to base_url.
|
|
168
|
+
:param params: Optional query parameters.
|
|
169
|
+
:param json_data: Optional JSON body.
|
|
170
|
+
:return: Parsed response payload.
|
|
171
|
+
"""
|
|
172
|
+
attempts: t.Dict[int, int] = {}
|
|
173
|
+
|
|
174
|
+
while True:
|
|
175
|
+
try:
|
|
176
|
+
return await self._send_once(
|
|
177
|
+
method,
|
|
178
|
+
path,
|
|
179
|
+
params=params,
|
|
180
|
+
json_data=json_data,
|
|
181
|
+
)
|
|
182
|
+
except ProviderResponseError as e:
|
|
183
|
+
policy = self._retry_policy
|
|
184
|
+
if policy is None:
|
|
185
|
+
raise
|
|
186
|
+
|
|
187
|
+
rule = policy.rule_for(e.code, e.message)
|
|
188
|
+
if rule is None:
|
|
189
|
+
raise
|
|
190
|
+
|
|
191
|
+
key = id(rule)
|
|
192
|
+
attempts[key] = attempts.get(key, 0) + 1
|
|
193
|
+
|
|
194
|
+
if attempts[key] >= rule.attempts:
|
|
195
|
+
raise RetryLimitError(
|
|
196
|
+
attempts=attempts[key],
|
|
197
|
+
max_attempts=rule.attempts,
|
|
198
|
+
last_error=e,
|
|
199
|
+
) from e
|
|
200
|
+
|
|
201
|
+
await asyncio.sleep(rule.delay(attempts[key] - 1))
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import typing as t
|
|
5
|
+
|
|
6
|
+
import aiohttp
|
|
7
|
+
|
|
8
|
+
from tonutils.exceptions import ProviderResponseError, CDN_CHALLENGE_MARKERS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HttpResponse:
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
async def read(cls, resp: aiohttp.ClientResponse) -> t.Any:
|
|
15
|
+
body = await resp.read()
|
|
16
|
+
if not body:
|
|
17
|
+
return ""
|
|
18
|
+
|
|
19
|
+
data = body.decode("utf-8", errors="replace").strip()
|
|
20
|
+
if not data:
|
|
21
|
+
return ""
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
return json.loads(data)
|
|
25
|
+
except (Exception,):
|
|
26
|
+
return data
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def raise_error(
|
|
30
|
+
cls,
|
|
31
|
+
*,
|
|
32
|
+
status: int,
|
|
33
|
+
url: str,
|
|
34
|
+
data: t.Any,
|
|
35
|
+
) -> None:
|
|
36
|
+
exc = cls._detect_proxy_error(data, status=status, url=url)
|
|
37
|
+
if exc is not None:
|
|
38
|
+
raise exc
|
|
39
|
+
|
|
40
|
+
message = cls._extract_error_message(data)
|
|
41
|
+
raise ProviderResponseError(
|
|
42
|
+
code=status,
|
|
43
|
+
message=message,
|
|
44
|
+
endpoint=url,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def _detect_proxy_error(
|
|
49
|
+
cls,
|
|
50
|
+
data: t.Any,
|
|
51
|
+
*,
|
|
52
|
+
status: int,
|
|
53
|
+
url: str,
|
|
54
|
+
) -> t.Optional[ProviderResponseError]:
|
|
55
|
+
body = (
|
|
56
|
+
" ".join(str(v) for v in data.values())
|
|
57
|
+
if isinstance(data, dict)
|
|
58
|
+
else str(data)
|
|
59
|
+
).lower()
|
|
60
|
+
|
|
61
|
+
for marker, message in CDN_CHALLENGE_MARKERS.items():
|
|
62
|
+
if marker in body:
|
|
63
|
+
return ProviderResponseError(
|
|
64
|
+
code=status,
|
|
65
|
+
message=message,
|
|
66
|
+
endpoint=url,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def _extract_error_message(data: t.Any) -> str:
|
|
73
|
+
if isinstance(data, dict):
|
|
74
|
+
lowered = {k.lower(): v for k, v in data.items()}
|
|
75
|
+
for key in ("error", "message", "detail", "description"):
|
|
76
|
+
if key in lowered and isinstance(lowered[key], str):
|
|
77
|
+
return lowered[key]
|
|
78
|
+
string_values = [str(v) for v in data.values() if isinstance(v, str)]
|
|
79
|
+
return "; ".join(string_values) if string_values else str(data)
|
|
80
|
+
|
|
81
|
+
if isinstance(data, list):
|
|
82
|
+
return "; ".join(map(str, data))
|
|
83
|
+
if isinstance(data, str):
|
|
84
|
+
return data
|
|
85
|
+
return repr(data)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
import aiohttp
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from tonutils.clients.http.providers.base import HttpProvider
|
|
9
|
+
from tonutils.clients.http.providers.tonapi.models import (
|
|
10
|
+
BlockchainAccountMethodResult,
|
|
11
|
+
BlockchainAccountResult,
|
|
12
|
+
BlockchainAccountTransactionsResult,
|
|
13
|
+
BlockchainConfigResult,
|
|
14
|
+
BlockchainMessagePayload,
|
|
15
|
+
)
|
|
16
|
+
from tonutils.types import NetworkGlobalID, RetryPolicy
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TonapiHttpProvider(HttpProvider):
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
network: NetworkGlobalID,
|
|
24
|
+
api_key: str,
|
|
25
|
+
base_url: t.Optional[str] = None,
|
|
26
|
+
timeout: float = 10.0,
|
|
27
|
+
session: t.Optional[aiohttp.ClientSession] = None,
|
|
28
|
+
headers: t.Optional[t.Dict[str, str]] = None,
|
|
29
|
+
cookies: t.Optional[t.Dict[str, str]] = None,
|
|
30
|
+
rps_limit: t.Optional[int] = None,
|
|
31
|
+
rps_period: float = 1.0,
|
|
32
|
+
retry_policy: t.Optional[RetryPolicy] = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
urls = {
|
|
35
|
+
NetworkGlobalID.MAINNET: "https://tonapi.io/v2",
|
|
36
|
+
NetworkGlobalID.TESTNET: "https://testnet.tonapi.io/v2",
|
|
37
|
+
}
|
|
38
|
+
base_url = base_url or urls[network]
|
|
39
|
+
headers = {**(headers or {}), "Authorization": f"Bearer {api_key}"}
|
|
40
|
+
|
|
41
|
+
super().__init__(
|
|
42
|
+
base_url=base_url,
|
|
43
|
+
session=session,
|
|
44
|
+
headers=headers,
|
|
45
|
+
cookies=cookies,
|
|
46
|
+
timeout=timeout,
|
|
47
|
+
rps_limit=rps_limit,
|
|
48
|
+
rps_period=rps_period,
|
|
49
|
+
retry_policy=retry_policy,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def _model(model: t.Type[BaseModel], data: t.Any) -> t.Any:
|
|
54
|
+
return model.model_validate(data)
|
|
55
|
+
|
|
56
|
+
async def blockchain_message(
|
|
57
|
+
self,
|
|
58
|
+
payload: BlockchainMessagePayload,
|
|
59
|
+
) -> None:
|
|
60
|
+
await self.send_http_request(
|
|
61
|
+
"POST",
|
|
62
|
+
"/blockchain/message",
|
|
63
|
+
json_data=payload.model_dump(),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
async def blockchain_config(
|
|
67
|
+
self,
|
|
68
|
+
) -> BlockchainConfigResult:
|
|
69
|
+
return self._model(
|
|
70
|
+
BlockchainConfigResult,
|
|
71
|
+
await self.send_http_request(
|
|
72
|
+
"GET",
|
|
73
|
+
"/blockchain/config",
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
async def blockchain_account(
|
|
78
|
+
self,
|
|
79
|
+
address: str,
|
|
80
|
+
) -> BlockchainAccountResult:
|
|
81
|
+
return self._model(
|
|
82
|
+
BlockchainAccountResult,
|
|
83
|
+
await self.send_http_request(
|
|
84
|
+
"GET",
|
|
85
|
+
f"/blockchain/accounts/{address}",
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
async def blockchain_account_transactions(
|
|
90
|
+
self,
|
|
91
|
+
address: str,
|
|
92
|
+
limit: int = 100,
|
|
93
|
+
after_lt: t.Optional[int] = None,
|
|
94
|
+
before_lt: t.Optional[int] = None,
|
|
95
|
+
sort_order: str = "desc",
|
|
96
|
+
) -> BlockchainAccountTransactionsResult:
|
|
97
|
+
params = {"limit": limit, "sort_order": sort_order}
|
|
98
|
+
if after_lt is not None:
|
|
99
|
+
params["after_lt"] = after_lt
|
|
100
|
+
if before_lt is not None and before_lt > 0:
|
|
101
|
+
params["before_lt"] = before_lt
|
|
102
|
+
|
|
103
|
+
return self._model(
|
|
104
|
+
BlockchainAccountTransactionsResult,
|
|
105
|
+
await self.send_http_request(
|
|
106
|
+
"GET",
|
|
107
|
+
f"/blockchain/accounts/{address}/transactions",
|
|
108
|
+
params=params,
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
async def blockchain_account_method(
|
|
113
|
+
self,
|
|
114
|
+
address: str,
|
|
115
|
+
method_name: str,
|
|
116
|
+
args: t.List[t.Any],
|
|
117
|
+
) -> BlockchainAccountMethodResult:
|
|
118
|
+
return self._model(
|
|
119
|
+
BlockchainAccountMethodResult,
|
|
120
|
+
await self.send_http_request(
|
|
121
|
+
"GET",
|
|
122
|
+
f"/blockchain/accounts/{address}/methods/{method_name}",
|
|
123
|
+
params={"args": args} if args else None,
|
|
124
|
+
),
|
|
125
|
+
)
|