Использование TON Connect
Этот раздел содержит полное руководство по работе с TON Connect с использованием библиотеки tonutils
. Описаны подключение кошелька, отправка транзакций и подпись данных, включая инициализацию, обработку запросов и проверку ответов.
Note
Перед использованием этих примеров рекомендуем ознакомиться с подробным руководством в разделе Рецепты: Интеграция TON Connect.
Реализация хранилища
import json
import os
from asyncio import Lock
from typing import Dict, Optional
import aiofiles
from tonutils.tonconnect import IStorage
class FileStorage(IStorage):
"""
File-based asynchronous implementation of TonConnect IStorage interface.
Stores key-value pairs in a local JSON file using asyncio-compatible file I/O.
:param file_path: Path to the JSON file used for persistent storage.
"""
def __init__(self, file_path: str):
self.file_path = file_path
self.lock = Lock()
if not os.path.exists(self.file_path):
with open(self.file_path, "w") as f:
json.dump({}, f)
async def _read_data(self) -> Dict[str, str]:
"""
Read the current contents of the JSON storage file.
:return: Dictionary containing all stored key-value pairs.
"""
async with self.lock:
async with aiofiles.open(self.file_path, "r") as f:
content = await f.read()
return json.loads(content) if content else {}
async def _write_data(self, data: Dict[str, str]) -> None:
"""
Write a new dictionary to the JSON storage file.
:param data: Key-value pairs to persist.
"""
async with self.lock:
async with aiofiles.open(self.file_path, "w") as f:
await f.write(json.dumps(data, indent=4))
async def set_item(self, key: str, value: str) -> None:
"""
Set a key-value pair in storage.
:param key: Key to set.
:param value: Value to associate with the key.
"""
data = await self._read_data()
data[key] = value
await self._write_data(data)
async def get_item(self, key: str, default_value: Optional[str] = None) -> Optional[str]:
"""
Retrieve the value associated with a key.
:param key: Key to retrieve.
:param default_value: Value to return if the key is not found.
:return: Stored value or default if not found.
"""
data = await self._read_data()
return data.get(key, default_value)
async def remove_item(self, key: str) -> None:
"""
Remove a key-value pair from storage.
:param key: Key to remove.
"""
data = await self._read_data()
if key in data:
del data[key]
await self._write_data(data)
Подключение кошелька
from storage import FileStorage
from tonutils.tonconnect import TonConnect
from tonutils.tonconnect.models import *
from tonutils.tonconnect.utils import generate_proof_payload
from tonutils.tonconnect.utils.exceptions import *
# Public URL to the application manifest.
# The manifest defines app metadata (name, icon, URL, permissions).
# Reference: https://github.com/ton-blockchain/ton-connect/blob/main/requests-responses.md#app-manifest
TC_MANIFEST_URL = "https://raw.githubusercontent.com/nessshon/tonutils/main/examples/tonconnect/tonconnect-manifest.json"
# Storage backend for persisting wallet connection data.
# File-based implementation using aiofiles.
TC_STORAGE = FileStorage("connection.json")
# Initialize TonConnect with storage, manifest, and fallback wallet list.
tc = TonConnect(
storage=TC_STORAGE,
manifest_url=TC_MANIFEST_URL,
wallets_fallback_file_path="./wallets.json"
)
@tc.on_event(Event.CONNECT)
async def on_wallet_connect(user_id: int, wallet: WalletInfo) -> None:
"""
Handle successful wallet connection.
:param user_id: Identifier of the connected user.
:param wallet: Connected wallet information.
WalletInfo contains:
- wallet.account: Address, chain ID, state init, and optional public key.
- wallet.ton_proof: Domain, payload, signature, and timestamp.
- wallet.device: Device information such as platform and app version.
Additional parameters can be passed via `connector.add_event_kwargs()`.
Example:
connector.add_event_kwargs(event=Event.CONNECT, comment="example")
"""
wallet_address = wallet.account.address.to_str(is_bounceable=False)
print(f"[Event CONNECT] Wallet {wallet_address} connected to user {user_id}.")
@tc.on_event(EventError.CONNECT)
async def on_wallet_connect_error(user_id: int, error: TonConnectError) -> None:
"""
Handle errors during wallet connection.
:param user_id: Identifier of the user attempting connection.
:param error: Raised error during the connection attempt.
Recognized error types:
- UserRejectsError: The user rejected the connection.
- RequestTimeoutError: Wallet did not respond within timeout.
Additional parameters can be passed via `connector.add_event_kwargs()`.
"""
if isinstance(error, UserRejectsError):
print(f"[EventError CONNECT] User {user_id} rejected the wallet connection.")
elif isinstance(error, RequestTimeoutError):
print(f"[EventError CONNECT] Connection request timed out for user {user_id}.")
else:
print(f"[EventError CONNECT] Connection error for user {user_id}: {error.message}")
@tc.on_event(Event.DISCONNECT)
async def on_wallet_disconnect(user_id: int, wallet: WalletInfo) -> None:
"""
Handle successful wallet disconnection.
:param user_id: Identifier of the user whose wallet was disconnected.
:param wallet: Disconnected wallet information.
Additional parameters can be passed via `connector.add_event_kwargs()`.
Example:
connector.add_event_kwargs(event=Event.DISCONNECT, comment="example")
"""
wallet_address = wallet.account.address.to_str(is_bounceable=False)
print(f"[Event DISCONNECT] Wallet {wallet_address} disconnected from user {user_id}.")
@tc.on_event(EventError.DISCONNECT)
async def on_wallet_disconnect_error(user_id: int, error: TonConnectError) -> None:
"""
Handle errors during wallet disconnection.
:param user_id: Identifier of the user whose wallet failed to disconnect.
:param error: Raised error during the disconnect attempt.
Recognized error types:
- RequestTimeoutError: Wallet did not respond to the disconnect request.
Additional parameters can be passed via `connector.add_event_kwargs()`.
"""
if isinstance(error, RequestTimeoutError):
print(f"[EventError DISCONNECT] Disconnect request timed out for user {user_id}.")
else:
print(f"[EventError DISCONNECT] Disconnect error for user {user_id}: {error.message}")
async def main() -> None:
user_id = 12345 # Example user identifier
# Initialize the connector for the user
connector = await tc.init_connector(user_id)
# Generate a TON Connect proof payload for authentication
ton_proof = generate_proof_payload()
# Check wallet connection
if not connector.connected:
print("Wallet not connected! Please connect the wallet to continue.")
# Get all available wallets
wallets = await tc.get_wallets()
# As an example, we will select the wallet with index 1 (Tonkeeper)
selected_wallet = wallets[1]
connect_url = await connector.connect_wallet(selected_wallet, ton_proof=ton_proof)
print(f"Please connect your wallet by visiting the following URL:\n{connect_url}")
print("Waiting for wallet connection...")
# Add additional parameters to be passed to event handlers
connector.add_event_kwargs(event=Event.TRANSACTION, comment="Hello from tonutils!")
# In addition to the handler, you can use a context manager to get the connection result
async with connector.connect_wallet_context() as response:
if isinstance(response, TonConnectError):
print(f"Connection error: {response.message}")
else:
if connector.wallet.verify_proof_payload(ton_proof):
wallet_address = response.account.address.to_str(is_bounceable=False)
print(f"Connected wallet: {wallet_address}")
else:
await connector.disconnect_wallet()
print("Proof verification failed.")
else:
wallet_address = connector.account.address.to_str(is_bounceable=False)
print(f"Wallet already connected: {wallet_address}")
user_input = input("Do you want to disconnect the wallet? (y/n): ").strip().lower()
if user_input == "y":
await connector.disconnect_wallet()
print("Wallet successfully disconnected.")
else:
print("Wallet remains connected.")
await tc.close_all()
if __name__ == "__main__":
import asyncio
try:
asyncio.run(main())
except (KeyboardInterrupt, SystemExit):
asyncio.run(tc.close_all())
Отправка транзакции
from storage import FileStorage
from tonutils.tonconnect import TonConnect
from tonutils.tonconnect.models import *
from tonutils.tonconnect.utils.exceptions import *
from tonutils.wallet.messages import TransferMessage
# Public URL to the application manifest.
# The manifest defines app metadata (name, icon, URL, permissions).
# Reference: https://github.com/ton-blockchain/ton-connect/blob/main/requests-responses.md#app-manifest
TC_MANIFEST_URL = "https://raw.githubusercontent.com/nessshon/tonutils/main/examples/tonconnect/tonconnect-manifest.json"
# Storage backend for persisting wallet connection data.
# File-based implementation using aiofiles.
TC_STORAGE = FileStorage("connection.json")
# Initialize TonConnect with storage, manifest, and fallback wallet list.
tc = TonConnect(
storage=TC_STORAGE,
manifest_url=TC_MANIFEST_URL,
wallets_fallback_file_path="./wallets.json"
)
@tc.on_event(Event.TRANSACTION)
async def on_transaction(transaction: SendTransactionResponse) -> None:
"""
Handle successful transaction event.
:param transaction: Transaction response containing BoC, hash, and message cell.
Transaction details:
- transaction.boc (str): Raw BoC of the outgoing message.
- transaction.normalized_hash (str): Hash of the message for tracking.
- transaction.cell (Cell): Parsed message cell.
Additional parameters can be passed via `connector.add_event_kwargs()`.
Example:
connector.add_event_kwargs(event=Event.TRANSACTION, comment="example")
"""
print(f"[Event TRANSACTION] Transaction sent successfully. Message hash: {transaction.normalized_hash}")
@tc.on_event(EventError.TRANSACTION)
async def on_transaction_error(error: TonConnectError) -> None:
"""
Handle errors during transaction sending.
:param error: Error raised when the transaction could not be processed.
Recognized error types:
- UserRejectsError: The user rejected the transaction.
- RequestTimeoutError: The wallet did not respond in time.
Additional parameters can be passed via `connector.add_event_kwargs()`.
"""
if isinstance(error, UserRejectsError):
print("[EventError TRANSACTION] User rejected the transaction.")
elif isinstance(error, RequestTimeoutError):
print("[EventError TRANSACTION] Transaction request timed out.")
else:
print(f"[EventError TRANSACTION] Failed to send transaction: {error.message}")
async def main() -> None:
user_id = 12345 # Example user identifier
# Initialize the connector for the user
connector = await tc.init_connector(user_id)
# Start the event processing loop
while True:
# Check wallet connection
if not connector.connected:
print("Wallet not connected! Please connect the wallet to continue.")
# Get all available wallets
wallets = await tc.get_wallets()
# As an example, we will select the wallet with index 1 (Tonkeeper)
selected_wallet = wallets[1]
connect_url = await connector.connect_wallet(selected_wallet)
print(f"Please connect your wallet by visiting the following URL:\n{connect_url}")
print("Waiting for wallet connection...")
async with connector.connect_wallet_context() as response:
if isinstance(response, TonConnectError):
print(f"Connection error: {response.message}")
continue
wallet_address = response.account.address.to_str(is_bounceable=False)
print(f"Connected wallet: {wallet_address}")
# If the wallet is connected, prompt the user to choose an action
call = input(
"\nChoose an action:\n"
"1. Send a transaction\n"
"2. Send a batch of transactions\n"
"3. Disconnect wallet\n"
"q. Quit\n"
"\nEnter your choice: "
).strip()
if call in ["1", "2"]:
if call == "1":
print("Preparing to send one transaction...")
rpc_request_id = await connector.send_transfer(
destination=connector.account.address,
amount=0.000000001,
body="Hello from tonutils!",
)
print("Request to send one transaction has been sent.")
else:
print("Preparing to send a batch of transactions...")
# Get the maximum number of messages supported in a transaction
max_messages = connector.device.get_max_supported_messages(connector.wallet)
print(f"Maximum number of messages: {max_messages}. Sending {max_messages} transactions...")
rpc_request_id = await connector.send_batch_transfer(
messages=[
TransferMessage(
destination=connector.account.address,
amount=0.000000001,
body="Hello from tonutils!",
) for _ in range(max_messages) # Create the maximum number of messages
]
)
print("Request to send a batch of transactions has been sent.")
# Add additional parameters to be passed to event handlers
connector.add_event_kwargs(event=Event.TRANSACTION, comment="Hello from tonutils!")
# Get the transaction status (whether it has been confirmed by the user in the wallet)
# Note: This is different from blockchain confirmation
is_pending = connector.is_request_pending(rpc_request_id)
print(f"Transaction is pending confirmation: {is_pending}")
# In addition to the handler, you can use a context manager to get the transaction result by rpc_request_id
async with connector.pending_request_context(rpc_request_id) as response:
if isinstance(response, TonConnectError):
print(f"Error sending transaction: {response.message}")
else:
print(f"Transaction successful! Hash: {response.normalized_hash}")
elif call == "3":
await connector.disconnect_wallet()
print("Wallet successfully disconnected.")
continue
elif call.lower() == "q":
print("Exiting the program...")
break
else:
print("Invalid choice! Please select a valid option.")
await tc.close_all()
if __name__ == "__main__":
import asyncio
try:
asyncio.run(main())
except (KeyboardInterrupt, SystemExit):
asyncio.run(tc.close_all())
Подпись данных
from pytoniq_core import begin_cell
from storage import FileStorage
from tonutils.tonconnect import TonConnect
from tonutils.tonconnect.models import *
from tonutils.tonconnect.utils.exceptions import *
# Public URL to the application manifest.
# The manifest defines app metadata (name, icon, URL, permissions).
# Reference: https://github.com/ton-blockchain/ton-connect/blob/main/requests-responses.md#app-manifest
TC_MANIFEST_URL = "https://raw.githubusercontent.com/nessshon/tonutils/main/examples/tonconnect/tonconnect-manifest.json"
# Storage backend for persisting wallet connection data.
# File-based implementation using aiofiles.
TC_STORAGE = FileStorage("connection.json")
# Initialize TonConnect with storage, manifest, and fallback wallet list.
tc = TonConnect(
storage=TC_STORAGE,
manifest_url=TC_MANIFEST_URL,
wallets_fallback_file_path="./wallets.json"
)
@tc.on_event(Event.SIGN_DATA)
async def on_sign_data(sign_data: SignDataResponse) -> None:
"""
Handle successful sign data event.
:param sign_data: Response containing signed data result.
SignDataResponse details:
- sign_data.result (str): Base64-encoded signed payload.
- sign_data.original (dict): Original payload that was signed.
Additional parameters can be passed via `connector.add_event_kwargs()`.
Example:
connector.add_event_kwargs(event=Event.SIGN_DATA, comment="example")
"""
print(f"[Event SIGN_DATA] Data to sign: {sign_data.result}")
@tc.on_event(EventError.SIGN_DATA)
async def on_sign_data_error(error: TonConnectError) -> None:
"""
Handle errors during sign data request.
:param error: Error raised when sign data could not be processed.
Recognized error types:
- UserRejectsError: The user rejected the sign data request.
- RequestTimeoutError: The wallet did not respond in time.
Additional parameters can be passed via `connector.add_event_kwargs()`.
"""
if isinstance(error, UserRejectsError):
print("[EventError SIGN_DATA] User rejected the sign data request.")
elif isinstance(error, RequestTimeoutError):
print("[EventError SIGN_DATA] Sign data request timed out.")
else:
print(f"[EventError SIGN_DATA] Failed to send sign data: {error.message}")
async def main() -> None:
user_id = 12345 # Example user identifier
# Initialize the connector for the user
connector = await tc.init_connector(user_id)
# Start the event processing loop
while True:
# Check wallet connection
if not connector.connected:
print("Wallet not connected. Please connect a wallet to continue.")
# Get all available wallets
wallets = await tc.get_wallets()
# As an example, we will select the wallet with index 1 (Tonkeeper)
selected_wallet = wallets[1]
connect_url = await connector.connect_wallet(selected_wallet)
print(f"Please connect your wallet by visiting the following URL:\n{connect_url}")
print("Waiting for wallet connection...")
async with connector.connect_wallet_context() as response:
if isinstance(response, TonConnectError):
print(f"Connection error: {response.message}")
continue
wallet_address = response.account.address.to_str(is_bounceable=False)
print(f"Connected wallet: {wallet_address}")
call = input(
"\nChoose an action:\n"
"1. Sign Text Data\n"
"2. Sign Binary Data\n"
"3. Sign Cell Data\n"
"d. Disconnect Wallet\n"
"q. Quit\n"
"\nEnter your choice: "
).strip().lower()
if call == "q":
print("Exiting the program...")
break
elif call == "d":
await connector.disconnect_wallet()
print("Wallet successfully disconnected.")
continue
elif call in {"1", "2", "3"}:
data = "Hello from tonutils!"
if call == "1":
payload = SignDataPayloadText(text=data)
elif call == "2":
payload = SignDataPayloadBinary(bytes=data.encode("utf-8"))
else:
payload = SignDataPayloadCell(
cell=begin_cell().store_uint(0, 32).store_snake_string(data).end_cell(),
schema="text_comment#00000000 text:Snakedata = InMsgBody;"
)
try:
connector.device.verify_sign_data_feature(connector.wallet, payload)
except WalletNotSupportFeatureError:
print("Wallet does not support sign data feature.")
continue
rpc_request_id = await connector.sign_data(payload)
# Add additional parameters to be passed to event handlers
connector.add_event_kwargs(event=Event.SIGN_DATA, comment="Hello from tonutils!")
# Get the transaction status (whether it has been confirmed by the user in the wallet)
# Note: This is different from blockchain confirmation
is_pending = connector.is_request_pending(rpc_request_id)
print(f"Sign data is pending confirmation: {is_pending}")
# In addition to the handler, you can use a context manager to get the sign data result by rpc_request_id
async with connector.pending_request_context(rpc_request_id) as response:
if isinstance(response, TonConnectError):
print(f"Error sending sign data: {response.message}")
else:
key = connector.wallet.account.public_key
if response.verify_sign_data(key):
print("Verified sign data.")
else:
print("Failed to verify sign data.")
else:
print("Invalid choice. Please select a valid option.")
await tc.close_all()
if __name__ == "__main__":
import asyncio
try:
asyncio.run(main())
except (KeyboardInterrupt, SystemExit):
asyncio.run(tc.close_all())