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.
- tonutils/{client → clients}/__init__.py +3 -11
- tonutils/clients/base.py +95 -0
- tonutils/clients/liteserver/__init__.py +3 -0
- tonutils/clients/liteserver/client.py +155 -0
- tonutils/clients/liteserver/stub.py +70 -0
- tonutils/clients/quicknode/__init__.py +3 -0
- tonutils/clients/quicknode/api.py +19 -0
- tonutils/clients/quicknode/client.py +20 -0
- tonutils/clients/tatum/__init__.py +3 -0
- tonutils/clients/tatum/api.py +28 -0
- tonutils/clients/tatum/client.py +24 -0
- tonutils/clients/tonapi/__init__.py +3 -0
- tonutils/clients/tonapi/api.py +93 -0
- tonutils/clients/tonapi/client.py +146 -0
- tonutils/clients/tonapi/models.py +34 -0
- tonutils/clients/toncenter/__init__.py +3 -0
- tonutils/clients/toncenter/api.py +91 -0
- tonutils/clients/toncenter/client.py +200 -0
- tonutils/clients/toncenter/models.py +70 -0
- tonutils/contracts/__init__.py +55 -0
- tonutils/contracts/base.py +155 -0
- tonutils/contracts/codes.py +30 -0
- tonutils/contracts/nft/__init__.py +21 -0
- tonutils/contracts/nft/collection.py +116 -0
- tonutils/contracts/nft/get_methods.py +124 -0
- tonutils/contracts/nft/item.py +119 -0
- tonutils/contracts/wallet/__init__.py +39 -0
- tonutils/contracts/wallet/base.py +300 -0
- tonutils/contracts/wallet/get_methods.py +145 -0
- tonutils/contracts/wallet/versions/__init__.py +45 -0
- tonutils/contracts/wallet/versions/hw.py +215 -0
- tonutils/contracts/wallet/versions/pp.py +88 -0
- tonutils/contracts/wallet/versions/v1.py +92 -0
- tonutils/contracts/wallet/versions/v2.py +81 -0
- tonutils/contracts/wallet/versions/v3.py +80 -0
- tonutils/contracts/wallet/versions/v4.py +102 -0
- tonutils/contracts/wallet/versions/v5.py +236 -0
- tonutils/exceptions.py +74 -25
- tonutils/protocols/__init__.py +9 -0
- tonutils/protocols/client.py +41 -0
- tonutils/protocols/contract.py +99 -0
- tonutils/protocols/wallet.py +116 -0
- tonutils/tonconnect/__init__.py +0 -11
- tonutils/types/__init__.py +187 -0
- tonutils/types/client.py +7 -0
- tonutils/types/common.py +39 -0
- tonutils/types/configs.py +79 -0
- tonutils/types/contract.py +79 -0
- tonutils/types/keystructs.py +91 -0
- tonutils/types/messages.py +142 -0
- tonutils/types/opcodes.py +15 -0
- tonutils/types/params.py +85 -0
- tonutils/types/stack.py +17 -0
- tonutils/types/tlb/__init__.py +87 -0
- tonutils/types/tlb/content.py +156 -0
- tonutils/types/tlb/contract.py +9 -0
- tonutils/types/tlb/msg.py +36 -0
- tonutils/types/tlb/nft.py +626 -0
- tonutils/types/tlb/text.py +53 -0
- tonutils/types/tlb/wallet.py +299 -0
- tonutils/utils/__init__.py +51 -0
- tonutils/utils/converters.py +58 -0
- tonutils/utils/msg_builders.py +82 -0
- tonutils/utils/parse_config.py +35 -0
- tonutils/utils/stack_codec.py +188 -0
- tonutils/utils/text_cipher.py +140 -0
- tonutils/utils/validations.py +23 -0
- tonutils/utils/value_utils.py +62 -0
- tonutils/utils/wallet_utils.py +55 -0
- {tonutils-0.5.1.dist-info → tonutils-0.6.0a1.dist-info}/METADATA +5 -10
- tonutils-0.6.0a1.dist-info/RECORD +76 -0
- {tonutils-0.5.1.dist-info → tonutils-0.6.0a1.dist-info}/licenses/LICENSE +1 -1
- tonutils/account.py +0 -32
- tonutils/cache.py +0 -82
- tonutils/client/_base.py +0 -292
- tonutils/client/lite.py +0 -163
- tonutils/client/quicknode.py +0 -33
- tonutils/client/tatum.py +0 -50
- tonutils/client/tonapi.py +0 -145
- tonutils/client/toncenter.py +0 -303
- tonutils/client/utils.py +0 -203
- tonutils/contract.py +0 -184
- tonutils/dns/__init__.py +0 -5
- tonutils/dns/categories.py +0 -15
- tonutils/dns/contract.py +0 -256
- tonutils/dns/op_codes.py +0 -1
- tonutils/dns/subdomain_collection/__init__.py +0 -5
- tonutils/dns/subdomain_collection/content.py +0 -18
- tonutils/dns/subdomain_collection/contract.py +0 -91
- tonutils/dns/subdomain_collection/data.py +0 -63
- tonutils/dns/subdomain_collection/op_codes.py +0 -5
- tonutils/dns/subdomain_manager/__init__.py +0 -5
- tonutils/dns/subdomain_manager/contract.py +0 -210
- tonutils/dns/subdomain_manager/data.py +0 -38
- tonutils/dns/subdomain_manager/op_codes.py +0 -1
- tonutils/dns/utils.py +0 -115
- tonutils/jetton/__init__.py +0 -15
- tonutils/jetton/content.py +0 -79
- tonutils/jetton/contract/__init__.py +0 -10
- tonutils/jetton/contract/base/__init__.py +0 -5
- tonutils/jetton/contract/base/master.py +0 -76
- tonutils/jetton/contract/stablecoin/__init__.py +0 -7
- tonutils/jetton/contract/stablecoin/master.py +0 -188
- tonutils/jetton/contract/stablecoin/op_codes.py +0 -15
- tonutils/jetton/contract/stablecoin/wallet.py +0 -130
- tonutils/jetton/contract/standard/__init__.py +0 -7
- tonutils/jetton/contract/standard/master.py +0 -141
- tonutils/jetton/contract/standard/op_codes.py +0 -11
- tonutils/jetton/contract/standard/wallet.py +0 -132
- tonutils/jetton/data.py +0 -165
- tonutils/jetton/dex/__init__.py +0 -0
- tonutils/jetton/dex/dedust/__init__.py +0 -5
- tonutils/jetton/dex/dedust/constants.py +0 -48
- tonutils/jetton/dex/dedust/factory.py +0 -362
- tonutils/jetton/dex/stonfi/__init__.py +0 -10
- tonutils/jetton/dex/stonfi/utils.py +0 -47
- tonutils/jetton/dex/stonfi/v1/__init__.py +0 -7
- tonutils/jetton/dex/stonfi/v1/pton/__init__.py +0 -5
- tonutils/jetton/dex/stonfi/v1/pton/constants.py +0 -19
- tonutils/jetton/dex/stonfi/v1/pton/pton.py +0 -78
- tonutils/jetton/dex/stonfi/v1/router/__init__.py +0 -5
- tonutils/jetton/dex/stonfi/v1/router/constants.py +0 -38
- tonutils/jetton/dex/stonfi/v1/router/router.py +0 -193
- tonutils/jetton/dex/stonfi/v2/__init__.py +0 -7
- tonutils/jetton/dex/stonfi/v2/pton/__init__.py +0 -5
- tonutils/jetton/dex/stonfi/v2/pton/constants.py +0 -21
- tonutils/jetton/dex/stonfi/v2/pton/pton.py +0 -102
- tonutils/jetton/dex/stonfi/v2/router/__init__.py +0 -5
- tonutils/jetton/dex/stonfi/v2/router/constants.py +0 -41
- tonutils/jetton/dex/stonfi/v2/router/router.py +0 -308
- tonutils/nft/__init__.py +0 -22
- tonutils/nft/content.py +0 -135
- tonutils/nft/contract/__init__.py +0 -0
- tonutils/nft/contract/base/__init__.py +0 -7
- tonutils/nft/contract/base/collection.py +0 -80
- tonutils/nft/contract/base/nft.py +0 -71
- tonutils/nft/contract/editable/__init__.py +0 -9
- tonutils/nft/contract/editable/collection.py +0 -341
- tonutils/nft/contract/editable/nft.py +0 -155
- tonutils/nft/contract/soulbound/__init__.py +0 -9
- tonutils/nft/contract/soulbound/collection.py +0 -277
- tonutils/nft/contract/soulbound/nft.py +0 -123
- tonutils/nft/contract/standard/__init__.py +0 -9
- tonutils/nft/contract/standard/collection.py +0 -257
- tonutils/nft/contract/standard/nft.py +0 -78
- tonutils/nft/data.py +0 -95
- tonutils/nft/marketplace/__init__.py +0 -0
- tonutils/nft/marketplace/getgems/__init__.py +0 -5
- tonutils/nft/marketplace/getgems/addresses.py +0 -8
- tonutils/nft/marketplace/getgems/contract/__init__.py +0 -5
- tonutils/nft/marketplace/getgems/contract/salev3r3.py +0 -161
- tonutils/nft/marketplace/getgems/data.py +0 -54
- tonutils/nft/marketplace/getgems/op_codes.py +0 -7
- tonutils/nft/op_codes.py +0 -19
- tonutils/nft/royalty_params.py +0 -29
- tonutils/tonconnect/connector.py +0 -699
- tonutils/tonconnect/models/__init__.py +0 -53
- tonutils/tonconnect/models/account.py +0 -57
- tonutils/tonconnect/models/chain.py +0 -10
- tonutils/tonconnect/models/device.py +0 -137
- tonutils/tonconnect/models/event.py +0 -33
- tonutils/tonconnect/models/proof.py +0 -80
- tonutils/tonconnect/models/request.py +0 -533
- tonutils/tonconnect/models/wallet.py +0 -248
- tonutils/tonconnect/provider/__init__.py +0 -5
- tonutils/tonconnect/provider/bridge.py +0 -581
- tonutils/tonconnect/provider/session.py +0 -104
- tonutils/tonconnect/storage/__init__.py +0 -7
- tonutils/tonconnect/storage/base.py +0 -39
- tonutils/tonconnect/storage/default.py +0 -40
- tonutils/tonconnect/tonconnect.py +0 -290
- tonutils/tonconnect/utils/__init__.py +0 -25
- tonutils/tonconnect/utils/exceptions.py +0 -140
- tonutils/tonconnect/utils/logger.py +0 -3
- tonutils/tonconnect/utils/verifiers.py +0 -255
- tonutils/tonconnect/utils/wallet_manager.py +0 -239
- tonutils/utils.py +0 -207
- tonutils/vanity/__init__.py +0 -5
- tonutils/vanity/contract.py +0 -35
- tonutils/vanity/data.py +0 -34
- tonutils/wallet/__init__.py +0 -31
- tonutils/wallet/contract/__init__.py +0 -24
- tonutils/wallet/contract/_base.py +0 -438
- tonutils/wallet/contract/highload.py +0 -505
- tonutils/wallet/contract/preprocessed.py +0 -291
- tonutils/wallet/contract/v2.py +0 -95
- tonutils/wallet/contract/v3.py +0 -94
- tonutils/wallet/contract/v4.py +0 -122
- tonutils/wallet/contract/v5.py +0 -193
- tonutils/wallet/data.py +0 -188
- tonutils/wallet/messages.py +0 -631
- tonutils/wallet/op_codes.py +0 -9
- tonutils/wallet/utils.py +0 -57
- tonutils-0.5.1.dist-info/RECORD +0 -131
- {tonutils-0.5.1.dist-info → tonutils-0.6.0a1.dist-info}/WHEEL +0 -0
- {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
|
-
}
|