tonutils 0.5.1__py3-none-any.whl → 0.6.0a1__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 (196) hide show
  1. tonutils/{client → clients}/__init__.py +3 -11
  2. tonutils/clients/base.py +95 -0
  3. tonutils/clients/liteserver/__init__.py +3 -0
  4. tonutils/clients/liteserver/client.py +155 -0
  5. tonutils/clients/liteserver/stub.py +70 -0
  6. tonutils/clients/quicknode/__init__.py +3 -0
  7. tonutils/clients/quicknode/api.py +19 -0
  8. tonutils/clients/quicknode/client.py +20 -0
  9. tonutils/clients/tatum/__init__.py +3 -0
  10. tonutils/clients/tatum/api.py +28 -0
  11. tonutils/clients/tatum/client.py +24 -0
  12. tonutils/clients/tonapi/__init__.py +3 -0
  13. tonutils/clients/tonapi/api.py +93 -0
  14. tonutils/clients/tonapi/client.py +146 -0
  15. tonutils/clients/tonapi/models.py +34 -0
  16. tonutils/clients/toncenter/__init__.py +3 -0
  17. tonutils/clients/toncenter/api.py +91 -0
  18. tonutils/clients/toncenter/client.py +200 -0
  19. tonutils/clients/toncenter/models.py +70 -0
  20. tonutils/contracts/__init__.py +55 -0
  21. tonutils/contracts/base.py +155 -0
  22. tonutils/contracts/codes.py +30 -0
  23. tonutils/contracts/nft/__init__.py +21 -0
  24. tonutils/contracts/nft/collection.py +116 -0
  25. tonutils/contracts/nft/get_methods.py +124 -0
  26. tonutils/contracts/nft/item.py +119 -0
  27. tonutils/contracts/wallet/__init__.py +39 -0
  28. tonutils/contracts/wallet/base.py +300 -0
  29. tonutils/contracts/wallet/get_methods.py +145 -0
  30. tonutils/contracts/wallet/versions/__init__.py +45 -0
  31. tonutils/contracts/wallet/versions/hw.py +215 -0
  32. tonutils/contracts/wallet/versions/pp.py +88 -0
  33. tonutils/contracts/wallet/versions/v1.py +92 -0
  34. tonutils/contracts/wallet/versions/v2.py +81 -0
  35. tonutils/contracts/wallet/versions/v3.py +80 -0
  36. tonutils/contracts/wallet/versions/v4.py +102 -0
  37. tonutils/contracts/wallet/versions/v5.py +236 -0
  38. tonutils/exceptions.py +74 -25
  39. tonutils/protocols/__init__.py +9 -0
  40. tonutils/protocols/client.py +41 -0
  41. tonutils/protocols/contract.py +99 -0
  42. tonutils/protocols/wallet.py +116 -0
  43. tonutils/tonconnect/__init__.py +0 -11
  44. tonutils/types/__init__.py +187 -0
  45. tonutils/types/client.py +7 -0
  46. tonutils/types/common.py +39 -0
  47. tonutils/types/configs.py +79 -0
  48. tonutils/types/contract.py +79 -0
  49. tonutils/types/keystructs.py +91 -0
  50. tonutils/types/messages.py +142 -0
  51. tonutils/types/opcodes.py +15 -0
  52. tonutils/types/params.py +85 -0
  53. tonutils/types/stack.py +17 -0
  54. tonutils/types/tlb/__init__.py +87 -0
  55. tonutils/types/tlb/content.py +156 -0
  56. tonutils/types/tlb/contract.py +9 -0
  57. tonutils/types/tlb/msg.py +36 -0
  58. tonutils/types/tlb/nft.py +626 -0
  59. tonutils/types/tlb/text.py +53 -0
  60. tonutils/types/tlb/wallet.py +299 -0
  61. tonutils/utils/__init__.py +51 -0
  62. tonutils/utils/converters.py +58 -0
  63. tonutils/utils/msg_builders.py +82 -0
  64. tonutils/utils/parse_config.py +35 -0
  65. tonutils/utils/stack_codec.py +188 -0
  66. tonutils/utils/text_cipher.py +140 -0
  67. tonutils/utils/validations.py +23 -0
  68. tonutils/utils/value_utils.py +62 -0
  69. tonutils/utils/wallet_utils.py +55 -0
  70. {tonutils-0.5.1.dist-info → tonutils-0.6.0a1.dist-info}/METADATA +5 -10
  71. tonutils-0.6.0a1.dist-info/RECORD +76 -0
  72. {tonutils-0.5.1.dist-info → tonutils-0.6.0a1.dist-info}/licenses/LICENSE +1 -1
  73. tonutils/account.py +0 -32
  74. tonutils/cache.py +0 -82
  75. tonutils/client/_base.py +0 -292
  76. tonutils/client/lite.py +0 -163
  77. tonutils/client/quicknode.py +0 -33
  78. tonutils/client/tatum.py +0 -50
  79. tonutils/client/tonapi.py +0 -145
  80. tonutils/client/toncenter.py +0 -303
  81. tonutils/client/utils.py +0 -203
  82. tonutils/contract.py +0 -184
  83. tonutils/dns/__init__.py +0 -5
  84. tonutils/dns/categories.py +0 -15
  85. tonutils/dns/contract.py +0 -256
  86. tonutils/dns/op_codes.py +0 -1
  87. tonutils/dns/subdomain_collection/__init__.py +0 -5
  88. tonutils/dns/subdomain_collection/content.py +0 -18
  89. tonutils/dns/subdomain_collection/contract.py +0 -91
  90. tonutils/dns/subdomain_collection/data.py +0 -63
  91. tonutils/dns/subdomain_collection/op_codes.py +0 -5
  92. tonutils/dns/subdomain_manager/__init__.py +0 -5
  93. tonutils/dns/subdomain_manager/contract.py +0 -210
  94. tonutils/dns/subdomain_manager/data.py +0 -38
  95. tonutils/dns/subdomain_manager/op_codes.py +0 -1
  96. tonutils/dns/utils.py +0 -115
  97. tonutils/jetton/__init__.py +0 -15
  98. tonutils/jetton/content.py +0 -79
  99. tonutils/jetton/contract/__init__.py +0 -10
  100. tonutils/jetton/contract/base/__init__.py +0 -5
  101. tonutils/jetton/contract/base/master.py +0 -76
  102. tonutils/jetton/contract/stablecoin/__init__.py +0 -7
  103. tonutils/jetton/contract/stablecoin/master.py +0 -188
  104. tonutils/jetton/contract/stablecoin/op_codes.py +0 -15
  105. tonutils/jetton/contract/stablecoin/wallet.py +0 -130
  106. tonutils/jetton/contract/standard/__init__.py +0 -7
  107. tonutils/jetton/contract/standard/master.py +0 -141
  108. tonutils/jetton/contract/standard/op_codes.py +0 -11
  109. tonutils/jetton/contract/standard/wallet.py +0 -132
  110. tonutils/jetton/data.py +0 -165
  111. tonutils/jetton/dex/__init__.py +0 -0
  112. tonutils/jetton/dex/dedust/__init__.py +0 -5
  113. tonutils/jetton/dex/dedust/constants.py +0 -48
  114. tonutils/jetton/dex/dedust/factory.py +0 -362
  115. tonutils/jetton/dex/stonfi/__init__.py +0 -10
  116. tonutils/jetton/dex/stonfi/utils.py +0 -47
  117. tonutils/jetton/dex/stonfi/v1/__init__.py +0 -7
  118. tonutils/jetton/dex/stonfi/v1/pton/__init__.py +0 -5
  119. tonutils/jetton/dex/stonfi/v1/pton/constants.py +0 -19
  120. tonutils/jetton/dex/stonfi/v1/pton/pton.py +0 -78
  121. tonutils/jetton/dex/stonfi/v1/router/__init__.py +0 -5
  122. tonutils/jetton/dex/stonfi/v1/router/constants.py +0 -38
  123. tonutils/jetton/dex/stonfi/v1/router/router.py +0 -193
  124. tonutils/jetton/dex/stonfi/v2/__init__.py +0 -7
  125. tonutils/jetton/dex/stonfi/v2/pton/__init__.py +0 -5
  126. tonutils/jetton/dex/stonfi/v2/pton/constants.py +0 -21
  127. tonutils/jetton/dex/stonfi/v2/pton/pton.py +0 -102
  128. tonutils/jetton/dex/stonfi/v2/router/__init__.py +0 -5
  129. tonutils/jetton/dex/stonfi/v2/router/constants.py +0 -41
  130. tonutils/jetton/dex/stonfi/v2/router/router.py +0 -308
  131. tonutils/nft/__init__.py +0 -22
  132. tonutils/nft/content.py +0 -135
  133. tonutils/nft/contract/__init__.py +0 -0
  134. tonutils/nft/contract/base/__init__.py +0 -7
  135. tonutils/nft/contract/base/collection.py +0 -80
  136. tonutils/nft/contract/base/nft.py +0 -71
  137. tonutils/nft/contract/editable/__init__.py +0 -9
  138. tonutils/nft/contract/editable/collection.py +0 -341
  139. tonutils/nft/contract/editable/nft.py +0 -155
  140. tonutils/nft/contract/soulbound/__init__.py +0 -9
  141. tonutils/nft/contract/soulbound/collection.py +0 -277
  142. tonutils/nft/contract/soulbound/nft.py +0 -123
  143. tonutils/nft/contract/standard/__init__.py +0 -9
  144. tonutils/nft/contract/standard/collection.py +0 -257
  145. tonutils/nft/contract/standard/nft.py +0 -78
  146. tonutils/nft/data.py +0 -95
  147. tonutils/nft/marketplace/__init__.py +0 -0
  148. tonutils/nft/marketplace/getgems/__init__.py +0 -5
  149. tonutils/nft/marketplace/getgems/addresses.py +0 -8
  150. tonutils/nft/marketplace/getgems/contract/__init__.py +0 -5
  151. tonutils/nft/marketplace/getgems/contract/salev3r3.py +0 -161
  152. tonutils/nft/marketplace/getgems/data.py +0 -54
  153. tonutils/nft/marketplace/getgems/op_codes.py +0 -7
  154. tonutils/nft/op_codes.py +0 -19
  155. tonutils/nft/royalty_params.py +0 -29
  156. tonutils/tonconnect/connector.py +0 -699
  157. tonutils/tonconnect/models/__init__.py +0 -53
  158. tonutils/tonconnect/models/account.py +0 -57
  159. tonutils/tonconnect/models/chain.py +0 -10
  160. tonutils/tonconnect/models/device.py +0 -137
  161. tonutils/tonconnect/models/event.py +0 -33
  162. tonutils/tonconnect/models/proof.py +0 -80
  163. tonutils/tonconnect/models/request.py +0 -533
  164. tonutils/tonconnect/models/wallet.py +0 -248
  165. tonutils/tonconnect/provider/__init__.py +0 -5
  166. tonutils/tonconnect/provider/bridge.py +0 -581
  167. tonutils/tonconnect/provider/session.py +0 -104
  168. tonutils/tonconnect/storage/__init__.py +0 -7
  169. tonutils/tonconnect/storage/base.py +0 -39
  170. tonutils/tonconnect/storage/default.py +0 -40
  171. tonutils/tonconnect/tonconnect.py +0 -290
  172. tonutils/tonconnect/utils/__init__.py +0 -25
  173. tonutils/tonconnect/utils/exceptions.py +0 -140
  174. tonutils/tonconnect/utils/logger.py +0 -3
  175. tonutils/tonconnect/utils/verifiers.py +0 -255
  176. tonutils/tonconnect/utils/wallet_manager.py +0 -239
  177. tonutils/utils.py +0 -207
  178. tonutils/vanity/__init__.py +0 -5
  179. tonutils/vanity/contract.py +0 -35
  180. tonutils/vanity/data.py +0 -34
  181. tonutils/wallet/__init__.py +0 -31
  182. tonutils/wallet/contract/__init__.py +0 -24
  183. tonutils/wallet/contract/_base.py +0 -438
  184. tonutils/wallet/contract/highload.py +0 -505
  185. tonutils/wallet/contract/preprocessed.py +0 -291
  186. tonutils/wallet/contract/v2.py +0 -95
  187. tonutils/wallet/contract/v3.py +0 -94
  188. tonutils/wallet/contract/v4.py +0 -122
  189. tonutils/wallet/contract/v5.py +0 -193
  190. tonutils/wallet/data.py +0 -188
  191. tonutils/wallet/messages.py +0 -631
  192. tonutils/wallet/op_codes.py +0 -9
  193. tonutils/wallet/utils.py +0 -57
  194. tonutils-0.5.1.dist-info/RECORD +0 -131
  195. {tonutils-0.5.1.dist-info → tonutils-0.6.0a1.dist-info}/WHEEL +0 -0
  196. {tonutils-0.5.1.dist-info → tonutils-0.6.0a1.dist-info}/top_level.txt +0 -0
@@ -1,581 +0,0 @@
1
- import asyncio
2
- import json
3
- from typing import Awaitable, Callable, Dict, Optional, Any
4
- from urllib.parse import urlencode, quote_plus
5
-
6
- import aiohttp
7
-
8
- from ..models import (
9
- Request,
10
- SendConnectRequest,
11
- SendDisconnectRequest,
12
- WalletApp,
13
- WalletInfo,
14
- Event
15
- )
16
- from ..provider.session import BridgeSession, SessionCrypto
17
- from ..storage import IStorage
18
- from ..utils.exceptions import TonConnectError
19
- from ..utils.logger import logger
20
-
21
-
22
- class HTTPBridge:
23
- """
24
- A class responsible for interacting with TonConnect via an HTTP-based bridge.
25
- It provides:
26
- - URL generation for SSE subscription and POST requests,
27
- - Receiving and processing SSE events,
28
- - Sending requests (RPC and connect/disconnect).
29
- """
30
-
31
- SSE_PATH = "events"
32
- POST_PATH = "message"
33
- DEFAULT_TTL = 300
34
- RESERVED_ID = -1
35
-
36
- def __init__(
37
- self,
38
- storage: IStorage,
39
- on_wallet_status_changed: Callable[..., Awaitable],
40
- on_rpc_response_received: Callable[..., Awaitable],
41
- api_tokens: Dict[str, str],
42
- wallet_app: Optional[WalletApp] = None,
43
- ) -> None:
44
- """
45
- :param storage: Interface for data storage.
46
- :param on_wallet_status_changed: Callback triggered when wallet status changes.
47
- :param on_rpc_response_received: Callback triggered when an RPC response is received.
48
- :param api_tokens: Dictionary of API tokens, where the key is the API name, and the value is the token.
49
- :param wallet_app: Optional wallet application data.
50
- """
51
- self.storage: IStorage = storage
52
- self.api_tokens = api_tokens
53
- self.wallet_app: Optional[WalletApp] = wallet_app
54
-
55
- # Manages current session data, cryptographic objects, etc.
56
- self.session = BridgeSession()
57
- # Holds futures by request ID so that we can retrieve the corresponding future upon response.
58
- self.pending_requests: Dict[int, asyncio.Future] = {}
59
- self.request_event_types: Dict[int, Event] = {}
60
-
61
- self._api_token: Optional[str] = self._choose_api_token(api_tokens, wallet_app)
62
- self._on_wallet_status_changed = on_wallet_status_changed
63
- self._on_rpc_response_received = on_rpc_response_received
64
-
65
- self._is_closed = False
66
- self._event_task: Optional[asyncio.Task] = None
67
- self._client_session: Optional[aiohttp.ClientSession] = None
68
-
69
- @property
70
- def closed(self) -> bool:
71
- """
72
- Indicates whether the bridge has been closed (i.e., it no longer processes events).
73
- """
74
- return self._is_closed
75
-
76
- @property
77
- def is_session_closed(self) -> bool:
78
- """
79
- Checks whether the current aiohttp client session is closed.
80
- """
81
- if not self._client_session:
82
- return True
83
- return self._client_session.closed
84
-
85
- @staticmethod
86
- def _choose_api_token(
87
- api_tokens: Dict[str, str],
88
- wallet_app: Optional[WalletApp] = None
89
- ) -> Optional[str]:
90
- """
91
- Selects the appropriate API token based on the wallet bridge URL.
92
-
93
- :param api_tokens: A dictionary {bridge_name: token}.
94
- :param wallet_app: WalletApp object containing the bridge URL.
95
- :return: The matching token or None if no match is found.
96
- """
97
- if not wallet_app:
98
- return None
99
-
100
- bridge_url = wallet_app.bridge_url or ""
101
- api_token = next((token for name, token in api_tokens.items() if name in bridge_url), None)
102
-
103
- if api_token is None:
104
- logger.debug(f"No matching API token found for bridge: {bridge_url}")
105
- else:
106
- logger.debug(f"Selected API token '{api_token}' for bridge: {bridge_url}")
107
-
108
- return api_token
109
-
110
- def _build_url(self, path: str, params: dict) -> str:
111
- """
112
- Constructs a full URL by appending query parameters to the base wallet_app bridge URL.
113
-
114
- :param path: An endpoint path (e.g., "events" or "message").
115
- :param params: A dictionary of query parameters.
116
- :return: The constructed URL as a string.
117
- """
118
- query_string = urlencode(params)
119
- return f"{self.wallet_app.bridge_url}/{path}?{query_string}" # type: ignore
120
-
121
- def _build_post_url(
122
- self,
123
- to: str,
124
- topic: Optional[str] = None,
125
- ttl: Optional[int] = None
126
- ) -> str:
127
- """
128
- Constructs a URL for sending a POST request to the bridge.
129
-
130
- :param to: The recipient’s public key.
131
- :param topic: The topic of the message (used for RPC methods).
132
- :param ttl: The message's time-to-live.
133
- :return: The constructed URL.
134
- """
135
- params = {
136
- "client_id": self.session.session_crypto.session_id,
137
- "to": to,
138
- "ttl": ttl or self.DEFAULT_TTL,
139
- "topic": topic,
140
- }
141
- params = {k: v for k, v in params.items() if v is not None}
142
- return self._build_url(self.POST_PATH, params)
143
-
144
- def _build_sse_url(self, last_event_id: Optional[str] = None) -> str:
145
- """
146
- Constructs a URL for subscribing to SSE events.
147
-
148
- :param last_event_id: The ID of the last processed event (for resuming).
149
- :return: The constructed SSE subscription URL.
150
- """
151
- params = {"client_id": self.session.session_crypto.session_id}
152
- if last_event_id is not None:
153
- params["last_event_id"] = last_event_id
154
- return self._build_url(self.SSE_PATH, params)
155
-
156
- @staticmethod
157
- def _is_telegram_url(url: str) -> bool:
158
- """
159
- Checks if a URL is a Telegram link (tg:// or t.me).
160
-
161
- :param url: The URL to check.
162
- :return: True if it's a Telegram URL, False otherwise.
163
- """
164
- return "tg" in url or "t.me" in url
165
-
166
- @staticmethod
167
- def _encode_telegram_params(params: str) -> str:
168
- """
169
- Encodes query parameters for a Telegram URL using the 'startapp' format.
170
-
171
- :param params: The raw query params as a string.
172
- :return: The encoded Telegram-specific string.
173
- """
174
- startapp = (
175
- "tonconnect-"
176
- + params
177
- .replace("+", "")
178
- .replace(".", "%2E")
179
- .replace("-", "%2D")
180
- .replace("_", "%5F")
181
- .replace("=", "__")
182
- .replace("&", "-")
183
- .replace("%", "--")
184
- .replace(":", "--3A")
185
- .replace("/", "--2F")
186
- )
187
- return f"startapp={startapp}"
188
-
189
- async def _process_event_data(self, raw_event: str) -> None:
190
- try:
191
- incoming_data = json.loads(raw_event)
192
- logger.debug(f"Parsed SSE event data: {incoming_data}")
193
- except json.JSONDecodeError:
194
- logger.debug("Failed to decode SSE data. Skipping.")
195
- return
196
-
197
- message = incoming_data.get("message")
198
- sender_pub_key = incoming_data.get("from")
199
-
200
- if not message or not sender_pub_key:
201
- logger.debug("Incomplete SSE data received. Missing 'message' or 'from'.")
202
- return
203
-
204
- decrypted_message = self.session.session_crypto.decrypt(
205
- message=message,
206
- sender_pub_key=sender_pub_key,
207
- )
208
- try:
209
- incoming_message = json.loads(decrypted_message)
210
- logger.debug(f"Decrypted incoming message: {incoming_message}")
211
- await self._handle_incoming_message(incoming_message, sender_pub_key)
212
- except json.JSONDecodeError:
213
- logger.debug("Decrypted message is not valid JSON. Skipping.")
214
- return
215
-
216
- async def _subscribe_to_events(self, url: str) -> None:
217
- """
218
- Subscribes to SSE events at the given URL and processes incoming messages.
219
-
220
- :param url: The SSE subscription URL.
221
- """
222
- logger.debug(f"Attempting to subscribe to SSE events at URL: {url}")
223
- max_retries, retry_count, retry_delay = 5, 0, 5
224
-
225
- while not self._is_closed:
226
- if not self._client_session:
227
- timeout = aiohttp.ClientTimeout(total=-1)
228
- headers = {"Authorization": f"Bearer {self._api_token}"} if self._api_token else {}
229
- self._client_session = aiohttp.ClientSession(headers=headers, timeout=timeout)
230
-
231
- try:
232
- async with self._client_session.get(url) as response:
233
- if response.status != 200:
234
- logger.debug(f"Failed to connect to bridge with status code: {response.status}")
235
- raise TonConnectError(f"Failed to connect to bridge: {response.status}")
236
-
237
- logger.debug("Connected to SSE stream successfully.")
238
- async for line in response.content:
239
- if self._is_closed:
240
- logger.debug("SSE subscription closed by user.")
241
- break
242
-
243
- decoded_line = line.decode().strip()
244
- if decoded_line.startswith("id:"):
245
- event_id = decoded_line[3:].strip()
246
- await self.storage.set_item(self.storage.KEY_LAST_EVENT_ID, event_id)
247
- if decoded_line.startswith("data:"):
248
- raw_event = decoded_line[5:].strip()
249
- if raw_event:
250
- await self._process_event_data(raw_event)
251
-
252
- # Reset retry counter upon successful connection
253
- retry_count = 0
254
-
255
- except (aiohttp.ClientPayloadError, RuntimeError) as e:
256
- if self._is_closed:
257
- logger.debug("SSE subscription closed by user.")
258
- break
259
-
260
- retry_count += 1
261
- logger.debug(f"Connection issue: {e}. Retrying in 5 seconds ({retry_count}/{max_retries})...")
262
- await asyncio.sleep(retry_delay)
263
-
264
- if retry_count >= max_retries:
265
- logger.debug("Max retries reached. Sending disconnect event.")
266
- await self._on_wallet_status_changed(SendDisconnectRequest().to_dict())
267
- await self.remove_session()
268
- break
269
-
270
- except asyncio.CancelledError:
271
- logger.debug("SSE subscription task was cancelled.")
272
- break
273
- except Exception as e:
274
- logger.exception(f"Unexpected error during SSE subscription: {e}")
275
- break
276
-
277
- logger.debug("SSE subscription task completed.")
278
- await self.pause_sse()
279
-
280
- async def _handle_incoming_message(
281
- self,
282
- incoming_message: Dict[str, Any],
283
- sender_pub_key: Optional[str] = None,
284
- ) -> None:
285
- """
286
- Handles a decrypted, parsed incoming message.
287
-
288
- :param incoming_message: The decrypted message as a dictionary.
289
- :param sender_pub_key: The sender’s public key, if available.
290
- """
291
- event_id = incoming_message.get("id")
292
- event_name = incoming_message.get("event")
293
-
294
- # Convert event_id to an integer if it's a string
295
- if isinstance(event_id, str):
296
- try:
297
- event_id = int(event_id)
298
- except ValueError:
299
- event_id = None
300
- logger.debug(f"Invalid event ID format: {incoming_message.get('id')}")
301
-
302
- connection = await self.get_stored_connection_data()
303
- last_wallet_event_id = connection.get("last_wallet_event_id")
304
-
305
- # If this is an RPC response (no 'event' field set)
306
- if event_name is None:
307
- future = self.pending_requests.get(event_id) # type: ignore
308
- logger.debug(f"Received RPC response for event ID {event_id}")
309
- if future is not None and not future.done():
310
- await self._on_rpc_response_received(incoming_message, event_id)
311
- return
312
-
313
- # It's a CONNECT or DISCONNECT event
314
- if event_id is not None:
315
- # Prevent reprocessing older events (except for CONNECT)
316
- if last_wallet_event_id is not None and event_id <= last_wallet_event_id and event_name != Event.CONNECT:
317
- logger.debug(f"Ignoring older event ID {event_id} <= {last_wallet_event_id}")
318
- return
319
- if event_name != Event.CONNECT:
320
- connection["last_wallet_event_id"] = event_id
321
- await self.storage.set_item(self.storage.KEY_CONNECTION, json.dumps(connection))
322
-
323
- if event_name == Event.CONNECT:
324
- if sender_pub_key:
325
- await self.update_session(incoming_message, sender_pub_key)
326
- elif event_name == Event.DISCONNECT:
327
- await self.remove_session()
328
-
329
- await self._on_wallet_status_changed(incoming_message)
330
-
331
- async def _send(
332
- self,
333
- request: str,
334
- receiver_public_key: str,
335
- topic: Optional[str] = None,
336
- ttl: Optional[int] = None,
337
- ) -> None:
338
- """
339
- Sends an encrypted message to the bridge via HTTP POST.
340
-
341
- :param request: The encrypted request string.
342
- :param receiver_public_key: The recipient’s public key (used in the URL).
343
- :param topic: Used in the query params (e.g., for RPC method).
344
- :param ttl: The time-to-live of the message on the bridge.
345
- """
346
- url = self._build_post_url(receiver_public_key, topic, ttl)
347
- headers = {"Authorization": f"Bearer {self._api_token}"} if self._api_token else {}
348
-
349
- logger.debug(f"Sending POST request to URL: {url}")
350
- async with aiohttp.ClientSession(headers=headers) as session:
351
- try:
352
- headers = {"Content-type": "text/plain;charset=UTF-8"}
353
- async with session.post(url, data=request, headers=headers) as response:
354
- if response.status != 200:
355
- logger.debug(f"Failed to send message with status code: {response.status}")
356
- raise TonConnectError(f"Failed to send message: {response.status}")
357
- except aiohttp.ClientError as e:
358
- logger.exception(f"HTTP Client Error while sending message: {e}")
359
- raise TonConnectError(f"HTTP Client Error: {e}")
360
-
361
- async def start_sse(self) -> None:
362
- """
363
- Starts the SSE subscription. Cancels any existing subscription task if present.
364
- """
365
- if self._is_closed:
366
- logger.debug("Attempted to start SSE while bridge is closed")
367
- return
368
-
369
- last_event_id = await self.storage.get_item(self.storage.KEY_LAST_EVENT_ID)
370
- url = self._build_sse_url(last_event_id)
371
-
372
- if self._event_task:
373
- self._event_task.cancel()
374
- try:
375
- await self._event_task
376
- logger.debug("Previous SSE task completed")
377
- except asyncio.CancelledError:
378
- logger.debug("Previous SSE task cancelled")
379
-
380
- self._event_task = asyncio.create_task(self._subscribe_to_events(url))
381
-
382
- async def pause_sse(self) -> None:
383
- """
384
- Pauses SSE subscription by closing the aiohttp session and canceling the SSE task.
385
- """
386
- logger.debug("Pausing SSE subscription")
387
- if self._client_session:
388
- await self._client_session.close()
389
- self._client_session = None
390
- logger.debug("aiohttp ClientSession closed for SSE subscription")
391
-
392
- if self._event_task and not self._event_task.done():
393
- self._event_task.cancel()
394
- try:
395
- await self._event_task
396
- except asyncio.CancelledError:
397
- pass
398
- self._event_task = None
399
-
400
- async def send_request(
401
- self,
402
- request: Request,
403
- rpc_request_id: int,
404
- ) -> Any:
405
- """
406
- Sends an RPC request to the wallet. Optionally waits for the response using a Future.
407
-
408
- :param request: A request object (Request or its subclasses).
409
- :param rpc_request_id: A unique ID to correlate requests and responses.
410
- :return: The response result if available, or None.
411
- """
412
- if not self.session or not self.session.wallet_public_key:
413
- logger.debug("Trying to send a request without an active session.")
414
- raise TonConnectError("Trying to send a request without an active session.")
415
-
416
- request.id = rpc_request_id
417
- message = json.dumps(request.to_dict())
418
- encoded_request = self.session.session_crypto.encrypt(
419
- message=message,
420
- receiver_pub_key=self.session.wallet_public_key,
421
- )
422
- await self._send(
423
- request=encoded_request,
424
- receiver_public_key=self.session.wallet_public_key,
425
- topic=request.method,
426
- )
427
-
428
- if rpc_request_id == self.RESERVED_ID:
429
- logger.debug("Reserved request ID used; no future to set.")
430
- return None
431
-
432
- logger.debug(f"Request ID {rpc_request_id} sent successfully. Waiting for response...")
433
- future = self.pending_requests.get(rpc_request_id)
434
- return await future if future else None
435
-
436
- async def get_stored_connection_data(self) -> Dict[str, Any]:
437
- """
438
- Retrieves stored session data from the storage.
439
-
440
- :return: A dictionary containing the connection data.
441
- """
442
- connection = await self.storage.get_item(self.storage.KEY_CONNECTION, "{}")
443
- return json.loads(connection) # type: ignore
444
-
445
- def generate_universal_url(
446
- self,
447
- request: Dict[str, Any],
448
- universal_url: str,
449
- redirect_url: str,
450
- ) -> str:
451
- """
452
- Generates a universal link for initiating a connection (e.g., for Telegram or another wallet).
453
-
454
- :param request: A dictionary containing the connect request data.
455
- :param universal_url: The base wallet universal URL.
456
- :param redirect_url: The URL to redirect the user back to after the action (e.g., "back").
457
- :return: The constructed universal link as a string.
458
- """
459
- version = 2
460
- session_id = self.session.session_crypto.session_id
461
- request_safe = quote_plus(json.dumps(request, separators=(",", ":")))
462
- query_params = f"v={version}&id={session_id}&r={request_safe}&ret={redirect_url}"
463
-
464
- if self._is_telegram_url(universal_url):
465
- # If it's a Telegram URL, convert the universal_url and query
466
- universal_url = WalletApp.universal_url_to_direct_url(universal_url)
467
- query_params = self._encode_telegram_params(query_params)
468
- return f"{universal_url}?{query_params}"
469
-
470
- return f"{universal_url}?{query_params}"
471
-
472
- async def connect(
473
- self,
474
- request: SendConnectRequest,
475
- universal_url: str,
476
- redirect_url: str = "back",
477
- ) -> str:
478
- """
479
- Initiates a connection (CONNECT):
480
- - Generates a new session,
481
- - Builds a universal link,
482
- - Starts the SSE subscription.
483
-
484
- :param request: The connect request object.
485
- :param universal_url: The wallet’s base universal link.
486
- :param redirect_url: The back URL (defaults to "back").
487
- :return: The final connect URL to be opened by the user.
488
- """
489
- await self.close_connection()
490
-
491
- session_crypto = SessionCrypto()
492
- self.session.bridge_url = self.wallet_app.bridge_url if self.wallet_app else ""
493
- self.session.session_crypto = session_crypto
494
-
495
- connect_url = self.generate_universal_url(
496
- request=request.to_dict(),
497
- universal_url=universal_url,
498
- redirect_url=redirect_url,
499
- )
500
- await self.start_sse()
501
-
502
- return connect_url
503
-
504
- async def restore_connection(self) -> WalletInfo:
505
- """
506
- Restores a previously established connection from storage (if CONNECT was previously called).
507
-
508
- :return: A WalletInfo object with the restored wallet data.
509
- :raises TonConnectError: If restoration fails due to missing or invalid data.
510
- """
511
- stored_connection = await self.storage.get_item(self.storage.KEY_CONNECTION)
512
- if not stored_connection:
513
- raise TonConnectError("Restore failed: no connection data found in storage.")
514
-
515
- connection = json.loads(stored_connection)
516
- if "session" not in connection:
517
- raise TonConnectError("Restore failed: no session data found in storage.")
518
-
519
- # Rebuild the session from stored data
520
- self.session = BridgeSession(stored=connection["session"])
521
- if self.session.bridge_url is None:
522
- raise TonConnectError("Restore failed: no bridge_url found in storage.")
523
-
524
- self.wallet_app = WalletApp.from_dict(connection.get("wallet_app") or {})
525
- self.session.bridge_url = self.wallet_app.bridge_url
526
- self._api_token = self._choose_api_token(self.api_tokens, self.wallet_app)
527
-
528
- connect_event = connection.get("connect_event")
529
- payload = connect_event.get("payload") if connect_event else None
530
- if payload is None:
531
- raise TonConnectError("Failed to restore connection: no payload found in stored response.")
532
-
533
- await self.start_sse()
534
- return WalletInfo.from_payload(payload)
535
-
536
- async def close_connection(self) -> None:
537
- """
538
- Stops the SSE subscription and clears the current session data.
539
- """
540
- if not self.is_session_closed:
541
- await self.pause_sse()
542
-
543
- self.session = BridgeSession()
544
- self.pending_requests.clear()
545
-
546
- async def update_session(self, event: Dict[str, Any], wallet_public_key: str) -> None:
547
- """
548
- Updates session data on CONNECT events and persists it to storage,
549
- enabling future reconnection.
550
-
551
- :param event: A dictionary with event data.
552
- :param wallet_public_key: The wallet's public key.
553
- """
554
- logger.debug(f"Updating session for wallet public key: {wallet_public_key}.")
555
- self.session.wallet_public_key = wallet_public_key
556
-
557
- connection = {
558
- "type": "http",
559
- "session": self.session.get_dict(),
560
- "last_wallet_event_id": event.get("id"),
561
- "connect_event": event,
562
- "next_rpc_request_id": 0,
563
- "wallet_app": self.wallet_app.to_dict() if self.wallet_app else {},
564
- }
565
-
566
- await self.storage.set_item(self.storage.KEY_CONNECTION, json.dumps(connection))
567
-
568
- async def remove_session(self) -> None:
569
- """
570
- Removes session data from storage and closes the current connection.
571
- """
572
- await self.close()
573
- await self.storage.remove_item(self.storage.KEY_CONNECTION)
574
- await self.storage.remove_item(self.storage.KEY_LAST_EVENT_ID)
575
-
576
- async def close(self) -> None:
577
- """
578
- Completely closes the HTTPBridge, including the subscription and session cleanup.
579
- """
580
- self._is_closed = True
581
- await self.close_connection()
@@ -1,104 +0,0 @@
1
- import base64
2
- from typing import Optional, Any, Dict, Union
3
-
4
- from nacl.encoding import HexEncoder
5
- from nacl.public import PublicKey, PrivateKey, Box
6
-
7
-
8
- class SessionCrypto:
9
- """
10
- Handles cryptographic operations for a session, including key pair
11
- generation, encryption, and decryption of messages using NaCl.
12
- """
13
-
14
- def __init__(self, private_key: Optional[Union[str, bytes]] = None) -> None:
15
- """
16
- Initializes SessionCrypto with a generated or provided private key.
17
-
18
- :param private_key: A hex-encoded private key (string or bytes). If None,
19
- a new PrivateKey is generated.
20
- """
21
- self.private_key = (
22
- PrivateKey(private_key, HexEncoder) # type: ignore
23
- if private_key else
24
- PrivateKey.generate()
25
- )
26
- # Session ID is derived from the public key in hex form
27
- self.session_id = self.private_key.public_key.encode().hex()
28
-
29
- def encrypt(self, message: str, receiver_pub_key: Union[str, bytes]) -> str:
30
- """
31
- Encrypts a message for a given receiver, identified by their public key.
32
-
33
- :param message: The plain text message to encrypt.
34
- :param receiver_pub_key: The receiver's public key (hex-encoded) as str or bytes.
35
- :return: The encrypted message as a base64-encoded string.
36
- """
37
- receiver_pub_key_obj = PublicKey(receiver_pub_key, encoder=HexEncoder) # type: ignore
38
- box = Box(self.private_key, receiver_pub_key_obj) # type: ignore
39
-
40
- message_bytes = message.encode()
41
- encrypted = box.encrypt(message_bytes)
42
- return base64.b64encode(encrypted).decode()
43
-
44
- def decrypt(self, message: str, sender_pub_key: Union[str, bytes]) -> str:
45
- """
46
- Decrypts a message using the sender's public key.
47
-
48
- :param message: The encrypted message as a base64-encoded string.
49
- :param sender_pub_key: The sender's public key (hex-encoded) as str or bytes.
50
- :return: The decrypted plain text message.
51
- """
52
- encrypted_message = base64.b64decode(message)
53
- sender_pub_key_obj = PublicKey(sender_pub_key, encoder=HexEncoder) # type: ignore
54
- box = Box(self.private_key, sender_pub_key_obj) # type: ignore
55
-
56
- decrypted = box.decrypt(encrypted_message)
57
- return decrypted.decode()
58
-
59
-
60
- class BridgeSession:
61
- """
62
- Stores session data for TonConnect, including cryptographic keys
63
- and bridge URL information. Facilitates loading/storing session
64
- details from/to a dictionary representation.
65
- """
66
-
67
- def __init__(self, stored: Optional[Dict[str, Any]] = None) -> None:
68
- """
69
- Initializes the BridgeSession with optional stored data.
70
-
71
- :param stored: A dictionary containing previously stored session fields:
72
- {
73
- "session_private_key": <hex-encoded private key>,
74
- "wallet_public_key": <hex-encoded wallet public key>,
75
- "bridge_url": <URL string>
76
- }
77
- """
78
- stored = stored or {}
79
- self.session_crypto = SessionCrypto(private_key=stored.get("session_private_key"))
80
- self.wallet_public_key = stored.get("wallet_public_key")
81
- self.bridge_url = stored.get("bridge_url")
82
-
83
- def get_dict(self) -> Dict[str, Any]:
84
- """
85
- Returns a dictionary representation of the session, suitable for storage.
86
-
87
- :return: A dictionary containing the private key, wallet public key,
88
- and bridge URL in a serializable format:
89
- {
90
- "session_private_key": <hex-encoded private key>,
91
- "wallet_public_key": <hex-encoded wallet public key>,
92
- "bridge_url": <URL string>
93
- }
94
- """
95
- session_private_key = (
96
- self.session_crypto.private_key.encode().hex()
97
- if self.session_crypto.private_key
98
- else None
99
- )
100
- return {
101
- "session_private_key": session_private_key,
102
- "wallet_public_key": self.wallet_public_key,
103
- "bridge_url": self.bridge_url
104
- }
@@ -1,7 +0,0 @@
1
- from .base import IStorage
2
- from .default import MemoryStorage
3
-
4
- __all__ = [
5
- "IStorage",
6
- "MemoryStorage",
7
- ]