Skip to content

Using TON Connect

This section provides a complete guide to working with TON Connect using the tonutils library. It covers wallet connection, transaction sending, and signing data, including initialization, request handling, and response verification.

Note

Before using these examples, we recommend reviewing the detailed guide in Cookbook: TON Connect Integration.


Storage Implementation

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)

Connect Wallet

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())

Send Transaction

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())

Sign Data

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())