Перейти к содержанию

Использование 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())