tonutils Python SDK

repository·main·Indexed 41 days ago

https://github.com/nessshon/tonutils

A comprehensive Python SDK for the TON Blockchain. It enables developers to manage wallets, execute transfers of TON, Jettons, and NFTs, and interact with smart contracts via lite servers or HTTP APIs. Features include a CLI for network monitoring, TON DNS resolution and record updates, ADNL address resolution via DHT, and tools for deploying contracts to vanity addresses.

Tokens
66K
Snippets
136
Records
160
Agent score
99%

What's inside tonutils

  1. Overview of tonutils features

    main

    Tonutils is a high-level, object-oriented Python SDK for the TON blockchain. It supports both native lite servers (ADNL) and HTTP API providers. Key capabilities include:

    • Providers: Support for HTTP APIs (Toncenter, Tonapi, Chainstack, QuickNode, Tatum) and native ADNL lite clients, including balancers with automatic failover.
    • Wallets: Create, import, and deploy standard wallet versions (V1–V5, Highload, Preprocessed).
    • Transfers: Support for TON, jettons, and NFTs, including batch, gasless, and encrypted transfers.
    • Contracts: Deploy, mint, and manage jetton, NFT, and DNS contracts.
    • Tools: Includes a block scanner, lite server and DHT status monitors, and a CLI.
  2. Core features of ton-connect

    main

    The ton-connect SDK provides the following capabilities for dApp backends and bots:

    • Wallet Connection: Connect wallets, restore, and manage per-user sessions.
    • Send Transaction: Build, send, and track transaction requests.
    • Sign Data: Request signatures over text, binary, and cell payloads.
    • TON Proof: Verify wallet ownership on your backend.
    • Storage: Supports file, in-memory, or custom key-value backends.
    • TonConnect-UI Interop: Import and export sessions created by TonConnect-UI frontends.
  3. Handle Sequential Transfers with SeqnoGuard

    main

    Standard wallets reject transactions if the seqno has already been used. If you attempt to send multiple transfers in rapid succession, the second one might fail because it uses the same on-chain seqno.

    To solve this, wrap your wallet in a SeqnoGuard. The guard waits for the on-chain seqno to advance after each successful send before allowing the next transaction to proceed.

    Configuration:

    • timeout: Maximum seconds to wait for the seqno to advance (default: 30.0).
    • poll_interval: Delay between seqno polls in seconds (default: 1.5).

    SeqnoGuard provides transfer() and transfer_message() methods that mirror the signatures of the underlying wallet.

    from ton_core import Address, NetworkGlobalID, to_nano
    from tonutils.clients import ToncenterClient
    from tonutils.contracts import SeqnoGuard, TONTransferBuilder, WalletV4R2
    
    MNEMONIC = "word1 word2 word3 ..."
    DESTINATION_1 = Address("UQ...")
    DESTINATION_2 = Address("UQ...")
    DESTINATION_3 = Address("UQ...")
    
    async def main() -> None:
        client = ToncenterClient(network=NetworkGlobalID.MAINNET)
        await client.connect()
    
        wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC)
    
        # Wrap wallet in SeqnoGuard for safe sequential sends
        guard = SeqnoGuard(wallet, timeout=30.0, poll_interval=1.0)
    
        # Each transfer is confirmed before the next one starts
        msg1 = await guard.transfer(
            destination=DESTINATION_1,
            amount=to_nano(0.01),
            body="First transfer",
        )
        print(f"Transaction hash: {msg1.normalized_hash}")
    
        msg2 = await guard.transfer(
            destination=DESTINATION_2,
            amount=to_nano(0.02),
            body="Second transfer",
        )
        print(f"Transaction hash: {msg2.normalized_hash}")
    
        # transfer_message() also works with builders
        msg3 = await guard.transfer_message(
            TONTransferBuilder(
                destination=DESTINATION_3,
                amount=to_nano(0.03),
                body="Third transfer via builder",
            )
        )
        print(f"Transaction hash: {msg3.normalized_hash}")
    
        await client.close()
    
    if __name__ == "__main__":
        import asyncio
        asyncio.run(main())
  4. Implement BlockScannerStorageProtocol for resumable scanning

    main

    To enable scanner.resume() across process restarts, provide a storage object that implements BlockScannerStorageProtocol. This object must persist the last processed masterchain seqno.

    The scanner calls set_mc_seqno after each fully processed masterchain block.

    Protocol Interface

    MethodDescription
    get_mc_seqno()Returns the last processed masterchain seqno, or None.
    set_mc_seqno(seqno)Persists the last processed masterchain seqno.

    If storage is omitted, resume() will raise a RuntimeError.

    from pathlib import Path
    
    class FileStorage:
        """Persists the last processed masterchain seqno to a plain-text file."""
    
        def __init__(self, path: str | Path) -> None:
            self._path = Path(path)
    
        async def get_mc_seqno(self) -> int | None:
            try:
                return int(self._path.read_text(encoding="utf-8").strip())
            except (OSError, ValueError):
                return None
    
        async def set_mc_seqno(self, seqno: int) -> None:
            self._path.write_text(f"{seqno}\n", encoding="utf-8")
  5. Understand Event Handling in TonConnect

    main

    TonConnect allows you to react to wallet connections, transactions, and errors using registered event handlers. This is particularly useful for long-lived services like Telegram bots where multiple user sessions are managed concurrently.

    While you can use await on methods like wait_connect() or wait_transaction() to handle results inline, registering event handlers allows the library to dispatch results asynchronously. Note that both styles can work simultaneously: a result will resolve the pending wait_* call AND dispatch the corresponding event.

  6. Understand TonConnect error types and properties

    main

    All TonConnect-related exceptions derive from TonConnectError (available in ton_connect.exceptions). Every error object contains the following attributes:

    • .code: A numeric RPC error code (defaults to 0 if not mapped).
    • .message: A string description of the error.
    • .info: An extended explanation of the error, or None if no extra info is available.

    Errors are surfaced in two ways:

    1. As return values: As the error element in the tuple returned by asynchronous waiting methods like wait_connect(), wait_transaction(), or wait_sign_data().
    2. As arguments: As the error argument passed to event handlers.
    3. As raised exceptions: Directly raised by methods such as connect() and disconnect().
  7. Generate a vanity contract address

    main

    To create a contract at a custom address (e.g., for branding), you must use a brute-force generator. The generator finds a salt that, when combined with the contract's initial code, produces the desired address pattern.

    1. Clone the generator: git clone https://github.com/ton-org/vanity.
    2. Run the generator using python3 src/generator.py.
    3. Specify the --owner (the wallet address that will authorize the deployment) and either --start (prefix) or --end (suffix) constraints.
    4. The generator outputs matches to addresses.jsonl. Each line is a JSON object containing the address, the init (code and prefix length), and the config. You will need this JSON line for deployment.
    python3 src/generator.py --owner {OWNER_ADDRESS} --end {SUFFIX} --case-sensitive
  8. Encrypt end-to-end messages with TextCipher

    main

    The library supports end-to-end encrypted comments using Elliptic Curve Diffie-Hellman (ECDH).

    To Encrypt:

    1. Retrieve the recipient's public key using get_public_key_get_method() on their wallet contract.
    2. Use TextCipher.encrypt() with the sender's address, sender's private key, and recipient's public key.
    3. Send the resulting body via a standard wallet transfer().

    Parameters for TextCipher.encrypt():

    • payload (str): The plaintext message.
    • sender_address (Address): The sender's address (used as salt for derivation; recipient must provide this to decrypt).
    • our_private_key (PrivateKey): The sender's private key.
    • their_public_key (PublicKey): The recipient's public key.
    from ton_core import Address, NetworkGlobalID, TextCipher, to_nano
    from tonutils.clients import ToncenterClient
    from tonutils.contracts import WalletV4R2, get_public_key_get_method
    
    MNEMONIC = "..."
    DESTINATION_ADDRESS = Address("UQ...")
    
    async def main() -> None:
        client = ToncenterClient(network=NetworkGlobalID.MAINNET)
        await client.connect()
    
        wallet, _, our_private_key, _ = WalletV4R2.from_mnemonic(client, MNEMONIC)
    
        # 1. Get recipient public key
        their_public_key = await get_public_key_get_method(client, DESTINATION_ADDRESS)
    
        # 2. Encrypt
        body = TextCipher.encrypt(
            payload="Hello!",
            sender_address=wallet.address,
            our_private_key=our_private_key,
            their_public_key=their_public_key,
        )
    
        # 3. Send
        await wallet.transfer(destination=DESTINATION_ADDRESS, amount=to_nano(0.01), body=body)
        await client.close()
  9. Mint a single NFT item

    main

    Minting an NFT item involves sending an NFTCollectionMintItemBody message to the collection contract. The transaction must be signed by the collection owner.

    Important Details:

    • Item Address Calculation: The address is not random; it is derived from the item_index, the nft_item_code, and the collection_address. Use NFTCollectionStandard.calculate_nft_item_address() to find it locally.
    • Forward Amount: You should include a forward_amount (typically 0.01 TON) to provide the newly deployed item with initial storage rent.
    • Metadata: The item's metadata URI is formed by appending a suffix (e.g., 0.json) to the collection's ITEMS_PREFIX_URI.
    from ton_core import (
        Address,
        NetworkGlobalID,
        NFTCollectionMintItemBody,
        NFTItemStandardMintRef,
        OffchainItemContent,
        to_nano,
    )
    from tonutils.clients import ToncenterClient
    from tonutils.contracts import NFTCollectionStandard, NFTItemStandard, WalletV4R2
    
    MNEMONIC = "word1 word2 word3 ..."
    OWNER_ADDRESS = Address("UQ...")
    NFT_COLLECTION_ADDRESS = Address("EQ...")
    NFT_ITEM_INDEX = 0
    NFT_ITEM_SUFFIX_URI = "0.json"
    
    async def main() -> None:
        client = ToncenterClient(network=NetworkGlobalID.MAINNET)
        await client.connect()
        wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC)
    
        nft_item_code = NFTItemStandard.get_default_code()
    
        # Item initialization data
        nft_item_ref = NFTItemStandardMintRef(
            owner_address=OWNER_ADDRESS,
            content=OffchainItemContent(suffix_uri=NFT_ITEM_SUFFIX_URI),
        )
    
        body = NFTCollectionMintItemBody(
            item_index=NFT_ITEM_INDEX,
            item_ref=nft_item_ref.serialize(),
            forward_amount=to_nano(0.01),
        )
    
        msg = await wallet.transfer(
            destination=NFT_COLLECTION_ADDRESS,
            amount=to_nano(0.025),
            body=body.serialize(),
        )
    
        nft_item_address = NFTCollectionStandard.calculate_nft_item_address(
            index=NFT_ITEM_INDEX,
            nft_item_code=nft_item_code,
            collection_address=NFT_COLLECTION_ADDRESS,
        )
    
        print(f"NFT item address: {nft_item_address.to_str()}")
        print(f"Transaction hash: {msg.normalized_hash}")
        await client.close()
  10. Pass Application Context to Handlers

    main

    Since handlers only receive the connector and event data, you should use the context system to pass application state (like database sessions or user objects). Context is a key-value store.

    1. Global Context: Set on the TonConnect instance. These are shared by all connectors.
    2. Per-Connector Context: Set during tc.create_connector(...). These are merged with the global context.

    Inside a handler, access context via the connector object using item access (e.g., connector["key"]).

    # Global context
    tc = TonConnect(..., db=my_db_session)
    tc["greeting"] = "Hello"
    
    # Per-connector context
    connector = tc.create_connector(session_key, user_id=123)
    
    # Accessing in handler
    @tc.on(Event.CONNECT)
    async def on_connect(connector: Connector, error: TonConnectError | None) -> None:
        user_id = connector["user_id"]
        db = connector["db"]
        greeting = connector["greeting"]