# Vanity Addresses Source: https://tonutils.ness.uz/cookbook/vanity Generate and deploy contracts and wallets at custom TON addresses with a chosen prefix or suffix. This guide explains how to create vanity addresses on the TON blockchain — contracts or wallets with custom patterns (for example, specific starting or ending characters) that make the address visually distinctive. ## Introduction Vanity addresses are often used for branding, better recognizability, or aesthetic purposes. A contract address on TON is a hash of its initial code and data, so a vanity address is found by brute force: a generator iterates salt values inside a special vanity contract's initial code until the resulting address matches the desired pattern. The vanity contract is then deployed at that address and immediately replaces its own code and data with those of the target contract. ## Vanity Contract Address To deploy a contract (for example, a jetton master) at a vanity address, first generate the address, then deploy through the vanity contract. ### Generate the Address Clone the generator repository: ```bash theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} git clone https://github.com/ton-org/vanity ``` Run the generator: ```bash theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} python3 src/generator.py --owner {OWNER_ADDRESS} --end {SUFFIX} --case-sensitive ``` * Replace `{OWNER_ADDRESS}` with the wallet address from which the deployment will be made. Only the owner can deploy through the generated vanity contract. * Replace `{SUFFIX}` with the desired ending of the address. Use `--start` instead of (or in addition to) `--end` for a prefix constraint. Matches are appended to `addresses.jsonl` — one JSON object per line containing the found `address`, the contract `init` (code and fixed prefix length), the generation `config`, and a `timestamp`. Copy the matching line; the deployment script parses it with `VanityResult.from_json`. ### Deploy the Contract The script below deploys a stablecoin jetton master at the generated vanity address. It builds the target contract's initial state, wraps its code and data in a `VanityDeployBody`, and sends it to the vanity contract together with the vanity `state_init`. The vanity contract validates the owner and replaces its own code and data with the payload, becoming the actual jetton master. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import ( Address, JettonMasterStablecoinData, NetworkGlobalID, OffchainContent, VanityDeployBody, to_nano, ) from tonutils.clients import ToncenterClient from tonutils.contracts import ( JettonMasterStablecoinV2, JettonWalletStablecoinV2, Vanity, VanityResult, WalletV4R2, ) # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # Jetton admin address (controls minting and metadata) ADMIN_ADDRESS = Address("UQ...") # Jetton metadata URI (TEP-64 off-chain format) JETTON_MASTER_URI = "https://example.com/jetton.json" # Vanity contract result JSON from the generator (one line of addresses.jsonl) VANITY_RESULT = '{"address":"EQ...","init":{"code":"te6cc...","fixedPrefixLength":...}' async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Create wallet instance from mnemonic (full access mode) wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Get stablecoin jetton wallet code jetton_wallet_code = JettonWalletStablecoinV2.get_default_code() # Configure jetton metadata jetton_master_content = OffchainContent(uri=JETTON_MASTER_URI) # Compose stablecoin jetton master initial data jetton_master_data = JettonMasterStablecoinData( admin_address=ADMIN_ADDRESS, content=jetton_master_content, jetton_wallet_code=jetton_wallet_code, ) # Create the target contract instance from initial data # Note: jetton_master.address here is NOT the final vanity address jetton_master = JettonMasterStablecoinV2.from_data( client=client, data=jetton_master_data.serialize(), ) # Parse vanity generator output JSON vanity_result = VanityResult.from_json(VANITY_RESULT) # Create vanity contract wrapper from generator result # Vanity contract validates owner, then replaces its code with payload vanity = Vanity.from_result( client=client, result=vanity_result, ) # Create vanity deploy message body with the target code and data body = VanityDeployBody( code=jetton_master.state_init.code, data=jetton_master.state_init.data, ) # Deploy jetton master via vanity contract msg = await wallet.transfer( destination=vanity.address, amount=to_nano(0.05), body=body.serialize(), state_init=vanity.state_init, ) print(f"Jetton master address: {vanity_result.address}") # Normalized hash of the signed external message (computed locally before # sending) — not the on-chain transaction hash. Use it to track whether # the message was accepted on-chain print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ## Vanity Wallet Address The mnemonic phrase and wallet ID generated by this tool are only compatible with `WalletV3R2`. To create a vanity wallet address using GPU acceleration, follow these steps. ### Check Requirements NVIDIA GPU (driver version 555.\* or later). ### Download Binary Download the `gpu-generator-linux` binary from the [latest release](https://github.com/ton-offline-storage/address-generator/releases). ### Run the Generator To start the generator with interactive input: ```bash theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} ./gpu-generator-linux ``` To run the generator with predefined constraints directly from the command line: ```bash theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} ./gpu-generator-linux -q "start[*][T][O][N] | end[1][2][3]" ``` Follow the on-screen instructions to monitor progress and view results. After a successful match, the tool displays the mnemonic phrase and the wallet ID for use with a `WalletV3R2` wallet. **Constraints Syntax** * **Allowed characters**: `A-Z`, `a-z`, `0-9`, `_`, `-` * **Start constraint** (after `UQ` prefix, third character): `start[A][P][P][L][E]` or `start[*][T][O][N]` * **End constraint**: `end[T][O][N]` or `end[Tt][Oo][Nn]` * **Combined constraints**: `start[*][T][O][N] & end[T][O][N]` * **Multiple variants (OR)**: `start[*][T][O][N] & end[T][O][N] | start[D][D][D] | end[0][0][0]` **Performance Reference** | Hardware | 5 chars | 6 chars | 7 chars | 8 chars | | - | - | - | - | - | | Intel i5-8350U | 4 min | 4 h 40 m | 12.5 days | > 2 years | | AMD Ryzen 5 3600 | 26 sec | 30 min | 31.5 h | 84 days | | NVIDIA GTX 1650 SUPER | 2 sec | 2 min | 2 h | 5.5 days | | NVIDIA RTX 4090 | under 1 sec | 13 sec | 13.5 min | 14.5 h | ### Use the Generated Wallet Import the wallet with the generated mnemonic and wallet ID. The wallet ID is passed as `subwallet_id` in a `WalletV3Config`: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID, WalletV3Config from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV3R2 # Mnemonic phrase from the generator MNEMONIC = "word1 word2 word3 ..." # Wallet ID from the generator WALLET_ID = 0 async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() wallet, public_key, private_key, mnemonic = WalletV3R2.from_mnemonic( client, MNEMONIC, config=WalletV3Config(subwallet_id=WALLET_ID), ) print(f"Address: {wallet.address.to_str()}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ## See Also * [Vanity Contract Generator](https://github.com/ton-org/vanity) * [Vanity Wallet Generator](https://github.com/ton-offline-storage/address-generator) Full runnable vanity contract deployment script on GitHub. # DNS Operations Source: https://tonutils.ness.uz/guide/examples/dns-operations Resolve TON DNS domains, read and update DNS records, and resolve ADNL addresses via DHT. TON DNS maps human-readable domains such as `example.ton` or `username.t.me` to on-chain values: wallet addresses, TON Site ADNL addresses, TON Storage bags, and subdomain resolvers. Each domain is an NFT item whose owner controls its DNS records. This section covers resolving domains, reading records from a DNS item, updating records, and resolving a site's ADNL address to network endpoints via DHT. ## Resolve a Domain Use `client.dnsresolve()` to resolve a domain to a record of a specific category. Resolution starts from the DNS root contract (taken from blockchain config parameter 4) and follows next-resolver records automatically, so `.ton` domains, `.t.me` domains, and subdomains all work. **Parameters:** * `domain` — domain name to query, e.g. `"example.ton"` or `"username.t.me"`; pre-encoded DNS bytes are also accepted (`str | bytes`). * `category` — DNS record type to retrieve (`DNSCategory`): `WALLET`, `SITE`, `STORAGE`, `DNS_NEXT_RESOLVER`, `TEXT`, or `ALL` (category `0`, requests all records and returns a raw `Cell`). * `dns_root_address` — custom DNS root address; defaults to the one from blockchain config (`Address`, optional). The method returns a parsed record object (`DNSRecordWallet`, `DNSRecordSite`, etc.), a raw `Cell` if the record cannot be parsed, or `None` if the record is not set. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import DNSCategory, NetworkGlobalID from tonutils.clients import ToncenterClient # Domain name to resolve (TON DNS format) # Supports .ton domains, .t.me domains, and subdomains (e.g., "example.ton", "username.t.me") DOMAIN = "ness.ton" async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Resolve DNS record for the specified domain and category wallet_record = await client.dnsresolve( domain=DOMAIN, category=DNSCategory.WALLET, ) # Display resolved wallet address # is_bounceable=False: standard for wallet contracts (UQ...) print(f"Linked wallet: {wallet_record.value.to_str(is_bounceable=False)}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ## Get DNS Records To read all records stored in a domain, load the DNS item contract with `TONDNSItem.from_address()` and iterate over its `dns_records` dictionary. Each value is a typed record object; check its type to interpret the value correctly. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import ( Address, DNSRecordDNSNextResolver, DNSRecordSite, DNSRecordStorage, DNSRecordText, DNSRecordWallet, NetworkGlobalID, ) from tonutils.clients import ToncenterClient from tonutils.contracts import TONDNSItem # DNS item address to query (e.g., .ton domain NFT) DNS_ITEM_ADDRESS = Address("EQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Load DNS item contract from the blockchain # Fetches current state and DNS records from on-chain data dns_item = await TONDNSItem.from_address( client=client, address=DNS_ITEM_ADDRESS, ) # dns_records: dict mapping category names to record objects for category, record in dns_item.dns_records.items(): # DNSRecordDNSNextResolver: resolver address for subdomain resolution if isinstance(record, DNSRecordDNSNextResolver): value = record.value.to_str() # DNSRecordWallet: wallet address linked to domain (non-bounceable format) elif isinstance(record, DNSRecordWallet): value = record.value.to_str(is_bounceable=False) # DNSRecordStorage: TON Storage BagID (hex format, uppercase) elif isinstance(record, DNSRecordStorage): value = record.value.as_hex.upper() # DNSRecordSite: ADNL address for TON Site (hex format) elif isinstance(record, DNSRecordSite): value = record.value.as_hex # DNSRecordText: arbitrary text record (dns_text#1eda) elif isinstance(record, DNSRecordText): value = record.value else: continue print(f"DNS record `{category}`: {value}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ## Set DNS Records To update a record, send a `ChangeDNSRecordBody` to the DNS item contract from the wallet that owns the domain. Each category expects a specific record type and value type: | Category | Record type | Value type | | - | - | - | | `DNSCategory.DNS_NEXT_RESOLVER` | `DNSRecordDNSNextResolver` | `Address` | | `DNSCategory.STORAGE` | `DNSRecordStorage` | `BagID` | | `DNSCategory.WALLET` | `DNSRecordWallet` | `Address` | | `DNSCategory.SITE` | `DNSRecordSite` | `ADNL` | | `DNSCategory.TEXT` | `DNSRecordText` | `str` | The transaction must be sent from the wallet that owns the DNS item, otherwise the contract rejects the change. The example below sets a wallet record; substitute the category and record type from the table to set other records. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import ( Address, ChangeDNSRecordBody, DNSCategory, DNSRecordWallet, NetworkGlobalID, to_nano, ) from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # DNS item address to update (e.g., .ton domain NFT) # Wallet must be domain owner to change DNS records DNS_ITEM_ADDRESS = Address("EQ...") # Wallet address to set as DNS record value WALLET_ADDRESS = Address("UQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Create wallet instance from mnemonic (full access mode) # Returns: (wallet, public_key, private_key, mnemonic) wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Construct change DNS record message body # category: DNS record category to update # record: DNS record value (wallet address for WALLET category) body = ChangeDNSRecordBody( category=DNSCategory.WALLET, record=DNSRecordWallet(WALLET_ADDRESS), ) # Send change DNS record transaction to the DNS item contract msg = await wallet.transfer( destination=DNS_ITEM_ADDRESS, amount=to_nano(0.05), body=body.serialize(), ) print(f"DNS item address: {DNS_ITEM_ADDRESS.to_str()}") # Normalized hash of the signed external message (computed locally before sending) # Not a blockchain transaction hash — use it to track whether the message # was accepted on-chain (e.g. via explorers, API queries, or your own checks) print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ## Resolve ADNL Address via DHT A TON Site's `SITE` record contains an ADNL address, not an IP address. To find the actual network endpoints, query the TON DHT with `DhtNetwork`, initialized from the network's global config via `from_network_config()`. `find_addresses()` returns the address list and the public key of the node behind the ADNL address, and raises `DhtValueNotFoundError` (from `tonutils.exceptions`) if the ADNL address is not found in the DHT. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import DNSCategory, NetworkGlobalID from tonutils.clients import ToncenterClient from tonutils.clients.dht import DhtNetwork from tonutils.exceptions import DhtValueNotFoundError # Domain name to resolve ADNL address for (must have a SITE record) DOMAIN = "foundation.ton" async def main() -> None: # Step 1: Resolve ADNL address from TON DNS client = ToncenterClient(network=NetworkGlobalID.MAINNET, rps_limit=1, rps_period=1.2) await client.connect() # Resolve SITE DNS record — it contains the ADNL address site_record = await client.dnsresolve( domain=DOMAIN, category=DNSCategory.SITE, ) await client.close() if site_record is None: print(f"No SITE record found for {DOMAIN}") return adnl = site_record.value print(f"Domain: {DOMAIN}") print(f"ADNL address: {adnl}") # Step 2: Resolve ADNL address to IP addresses via DHT # Initialize DHT network client from mainnet global config dht = DhtNetwork.from_network_config(NetworkGlobalID.MAINNET) await dht.connect() print("\nSearching for addresses in DHT...") try: addr_list, pub_key = await dht.find_addresses(adnl) except DhtValueNotFoundError: print("ADNL address not found in DHT") await dht.close() return print(f"Public key: {pub_key.hex()}") print(f"Addresses: {len(addr_list.addrs)} found") for addr in addr_list.addrs: print(f" {addr.endpoint}") await dht.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` DNS resolution issues multiple sequential get-method calls (one per resolver hop plus a config fetch), so the example lowers the client's request rate with `rps_limit=1, rps_period=1.2` to stay within the free Toncenter rate limits. Full runnable scripts for all DNS operations on GitHub. # Jetton Operations Source: https://tonutils.ness.uz/guide/examples/jetton-operations Deploy, mint, burn, and administer Stablecoin V2 and Standard jettons with tonutils. This section covers working with jettons: **Stablecoin Jettons** (the sharded Stablecoin V2 contract with admin-controlled minting — the successor of the V1 contract used by tokens such as USD₮ and NOT) and **Standard Jettons** (the classic TEP-74 implementation). Each subsection is a full runnable script: deployment, minting, burning, and administration. ## Stablecoin Jetton ### Deploy Jetton Master Any funded wallet can deploy the master contract; the `ADMIN_ADDRESS` set in the initial data controls minting and metadata afterward. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import ( Address, JettonMasterStablecoinData, JettonTopUpBody, NetworkGlobalID, OffchainContent, to_nano, ) from tonutils.clients import ToncenterClient from tonutils.contracts import ( JettonMasterStablecoinV2, JettonWalletStablecoinV2, WalletV4R2, ) # Mnemonic phrase used to derive the wallet's private key MNEMONIC = "word1 word2 word3 ..." # Jetton admin address (controls minting and metadata) ADMIN_ADDRESS = Address("UQ...") # Jetton metadata URI (TEP-64 off-chain format) # Points to JSON with jetton metadata (name, symbol, decimals, image) JETTON_MASTER_URI = "https://example.com/jetton.json" async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Get stablecoin jetton wallet code jetton_wallet_code = JettonWalletStablecoinV2.get_default_code() # Configure jetton metadata (off-chain format) jetton_master_content = OffchainContent(uri=JETTON_MASTER_URI) # Compose stablecoin jetton master initial data jetton_master_data = JettonMasterStablecoinData( admin_address=ADMIN_ADDRESS, content=jetton_master_content, jetton_wallet_code=jetton_wallet_code, ) # Create the master contract instance from initial data # The address is derived deterministically from code + data hash jetton_master = JettonMasterStablecoinV2.from_data( client=client, data=jetton_master_data.serialize(), ) # Empty top-up body used for jetton master deployment body = JettonTopUpBody() # Deploy the jetton master contract via a state_init message msg = await wallet.transfer( destination=jetton_master.address, amount=to_nano(0.05), body=body.serialize(), state_init=jetton_master.state_init, ) print(f"Jetton master address: {jetton_master.address.to_str()}") # Normalized hash of the signed external message (computed locally # before sending) — not the on-chain transaction hash print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Mint Jetton Only the jetton admin can mint; the outer `forward_amount` is the TON attached to deploy and fund the recipient's jetton wallet, while the inner one (1 nanoton) triggers the transfer notification. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import ( Address, JettonInternalTransferBody, JettonMintBody, NetworkGlobalID, to_nano, ) from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Mnemonic phrase used to derive the wallet's private key MNEMONIC = "word1 word2 word3 ..." # Recipient address (regular wallet, not jetton wallet) DESTINATION_ADDRESS = Address("UQ...") # Deployed jetton master contract address JETTON_MASTER_ADDRESS = Address("EQ...") # Amount of jettons to mint in base units # Multiply by 10^decimals for whole tokens (e.g., 1 * 10^9 = 1 jetton with 9 decimals) JETTON_AMOUNT_TO_MINT = to_nano(1, decimals=9) async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must be the jetton admin to mint successfully wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Internal transfer sent from the jetton master to the recipient's jetton wallet # forward_amount: nanotons forwarded to recipient (1 nanoton triggers transfer_notification) internal_transfer = JettonInternalTransferBody( jetton_amount=JETTON_AMOUNT_TO_MINT, response_address=wallet.address, forward_amount=1, ) # Mint message body (stablecoin-specific implementation) # forward_amount: TON attached to deploy/fund the recipient's jetton wallet body = JettonMintBody( internal_transfer=internal_transfer, destination=DESTINATION_ADDRESS, forward_amount=to_nano(0.1), ) # amount must cover forward_amount + gas fees msg = await wallet.transfer( destination=JETTON_MASTER_ADDRESS, amount=to_nano(0.125), body=body.serialize(), ) print(f"Jetton master address: {JETTON_MASTER_ADDRESS.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Burn Jetton Burn is supported in stablecoin V2 contracts only. The original stablecoin V1 contract (e.g. USD₮) does not implement the burn opcode. Any jetton holder can burn its own balance; the message goes to the holder's jetton wallet (resolved via the master's `get_wallet_address` get-method), not to the jetton master. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, JettonBurnBody, NetworkGlobalID, to_nano from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2, get_wallet_address_get_method # Mnemonic phrase used to derive the wallet's private key MNEMONIC = "word1 word2 word3 ..." # Deployed jetton master contract address JETTON_MASTER_ADDRESS = Address("EQ...") # Amount of jettons to burn in base units JETTON_AMOUNT_TO_BURN = to_nano(1, decimals=9) async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must own jettons to burn successfully wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Get the owner's jetton wallet address via the master's get_wallet_address get-method jetton_wallet_address = await get_wallet_address_get_method( client=client, address=JETTON_MASTER_ADDRESS, owner_address=wallet.address, ) # Burn message body # response_address: address to receive excess TON and burn confirmation body = JettonBurnBody( jetton_amount=JETTON_AMOUNT_TO_BURN, response_address=wallet.address, ) # Send the burn message to the owner's jetton wallet (not the jetton master) msg = await wallet.transfer( destination=jetton_wallet_address, body=body.serialize(), amount=to_nano(0.05), ) print(f"Jetton wallet address: {jetton_wallet_address.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Change Admin Only the current admin can transfer admin rights to a new address. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, JettonChangeAdminBody, NetworkGlobalID, to_nano from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Mnemonic phrase used to derive the wallet's private key MNEMONIC = "word1 word2 word3 ..." # New admin address (will control jetton minting and metadata) ADMIN_ADDRESS = Address("UQ...") # Deployed jetton master contract address JETTON_MASTER_ADDRESS = Address("EQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must be the current jetton admin to change admin successfully wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Change admin message body (stablecoin-specific implementation) body = JettonChangeAdminBody(admin_address=ADMIN_ADDRESS) msg = await wallet.transfer( destination=JETTON_MASTER_ADDRESS, body=body.serialize(), amount=to_nano(0.05), ) print(f"Jetton master address: {JETTON_MASTER_ADDRESS.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Drop Admin Dropping the admin is irreversible. The admin address is set to null, and no one can mint tokens or change metadata afterward. Only the current admin can drop admin rights, which sets the admin address to null. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, JettonDropAdminBody, NetworkGlobalID, to_nano from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Mnemonic phrase used to derive the wallet's private key MNEMONIC = "word1 word2 word3 ..." # Deployed jetton master contract address JETTON_MASTER_ADDRESS = Address("EQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must be the current jetton admin to drop admin successfully wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Drop admin message body: removes the admin address from the jetton master body = JettonDropAdminBody() msg = await wallet.transfer( destination=JETTON_MASTER_ADDRESS, body=body.serialize(), amount=to_nano(0.05), ) print(f"Jetton master address: {JETTON_MASTER_ADDRESS.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Change Content Only the current admin can replace the jetton metadata URI. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import ( Address, JettonChangeContentBody, NetworkGlobalID, OffchainContent, to_nano, ) from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Mnemonic phrase used to derive the wallet's private key MNEMONIC = "word1 word2 word3 ..." # Deployed jetton master contract address JETTON_MASTER_ADDRESS = Address("EQ...") # New jetton metadata URI (TEP-64 off-chain format) JETTON_MASTER_URI = "https://example.com/jetton.json" async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must be the current jetton admin to change content successfully wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Configure new jetton metadata (off-chain format) jetton_master_content = OffchainContent(uri=JETTON_MASTER_URI) # Change content message body (stablecoin-specific implementation) body = JettonChangeContentBody(content=jetton_master_content) msg = await wallet.transfer( destination=JETTON_MASTER_ADDRESS, body=body.serialize(), amount=to_nano(0.05), ) print(f"Jetton master address: {JETTON_MASTER_ADDRESS.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ## Standard Jetton ### Deploy Jetton Master `JettonMasterStandardData` also accepts `OnchainContent` from `ton_core` (a dictionary of TEP-64 metadata fields) for fully on-chain metadata instead of `OffchainContent`. Any funded wallet can deploy the master contract; the `ADMIN_ADDRESS` set in the initial data controls minting and metadata afterward. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import ( Address, JettonMasterStandardData, JettonTopUpBody, NetworkGlobalID, OffchainContent, to_nano, ) from tonutils.clients import ToncenterClient from tonutils.contracts import JettonMasterStandard, JettonWalletStandard, WalletV4R2 # Mnemonic phrase used to derive the wallet's private key MNEMONIC = "word1 word2 word3 ..." # Jetton admin address (controls minting and metadata) ADMIN_ADDRESS = Address("UQ...") # Jetton metadata URI (TEP-64 off-chain format) JETTON_MASTER_URI = "https://example.com/jetton.json" async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Get default jetton wallet code jetton_wallet_code = JettonWalletStandard.get_default_code() # Configure jetton metadata (off-chain format) jetton_master_content = OffchainContent(uri=JETTON_MASTER_URI) # Compose jetton master initial data jetton_master_data = JettonMasterStandardData( admin_address=ADMIN_ADDRESS, content=jetton_master_content, jetton_wallet_code=jetton_wallet_code, ) # Create the master contract instance from initial data # The address is derived deterministically from code + data hash jetton_master: JettonMasterStandard = JettonMasterStandard.from_data( client=client, data=jetton_master_data.serialize(), ) # Empty top-up body used for jetton master deployment body = JettonTopUpBody() # Deploy the jetton master contract via a state_init message msg = await wallet.transfer( destination=jetton_master.address, amount=to_nano(0.05), body=body.serialize(), state_init=jetton_master.state_init, ) print(f"Jetton master address: {jetton_master.address.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Mint Jetton Only the jetton admin can mint; the outer `forward_amount` is the TON attached to deploy and fund the recipient's jetton wallet, while the inner one (1 nanoton) triggers the transfer notification. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import ( Address, JettonInternalTransferBody, JettonStandardMintBody, NetworkGlobalID, to_nano, ) from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Mnemonic phrase used to derive the wallet's private key MNEMONIC = "word1 word2 word3 ..." # Recipient address (regular wallet, not jetton wallet) DESTINATION_ADDRESS = Address("UQ...") # Deployed jetton master contract address JETTON_MASTER_ADDRESS = Address("EQ...") # Amount of jettons to mint in base units JETTON_AMOUNT_TO_MINT = to_nano(1, decimals=9) async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must be the jetton admin to mint successfully wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Internal transfer sent from the jetton master to the recipient's jetton wallet # forward_amount: nanotons forwarded to recipient (1 nanoton triggers transfer_notification) internal_transfer = JettonInternalTransferBody( jetton_amount=JETTON_AMOUNT_TO_MINT, response_address=wallet.address, forward_amount=1, ) # Mint message body # forward_amount: TON attached to deploy/fund the recipient's jetton wallet body = JettonStandardMintBody( internal_transfer=internal_transfer, destination=DESTINATION_ADDRESS, forward_amount=to_nano(0.05), ) # amount must cover forward_amount + gas fees msg = await wallet.transfer( destination=JETTON_MASTER_ADDRESS, amount=to_nano(0.075), body=body.serialize(), ) print(f"Jetton master address: {JETTON_MASTER_ADDRESS.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Change Admin Only the current admin can transfer admin rights to a new address. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, JettonStandardChangeAdminBody, NetworkGlobalID, to_nano from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Mnemonic phrase used to derive the wallet's private key MNEMONIC = "word1 word2 word3 ..." # New admin address (will control jetton minting and metadata) ADMIN_ADDRESS = Address("UQ...") # Deployed jetton master contract address JETTON_MASTER_ADDRESS = Address("EQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must be the current jetton admin to change admin successfully wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Change admin message body body = JettonStandardChangeAdminBody(admin_address=ADMIN_ADDRESS) msg = await wallet.transfer( destination=JETTON_MASTER_ADDRESS, body=body.serialize(), amount=to_nano(0.05), ) print(f"Jetton master address: {JETTON_MASTER_ADDRESS.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Change Content Only the current admin can replace the jetton metadata URI. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import ( Address, JettonStandardChangeContentBody, NetworkGlobalID, OffchainContent, to_nano, ) from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Mnemonic phrase used to derive the wallet's private key MNEMONIC = "word1 word2 word3 ..." # Deployed jetton master contract address JETTON_MASTER_ADDRESS = Address("EQ...") # New jetton metadata URI (TEP-64 off-chain format) JETTON_MASTER_URI = "https://example.com/jetton.json" async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must be the current jetton admin to change content successfully wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Configure new jetton metadata (off-chain format) jetton_master_content = OffchainContent(uri=JETTON_MASTER_URI) # Change content message body body = JettonStandardChangeContentBody(content=jetton_master_content) msg = await wallet.transfer( destination=JETTON_MASTER_ADDRESS, body=body.serialize(), amount=to_nano(0.05), ) print(f"Jetton master address: {JETTON_MASTER_ADDRESS.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Burn Jetton Any jetton holder can burn its own balance; the message goes to the holder's jetton wallet (resolved via the master's `get_wallet_address` get-method), not to the jetton master. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, JettonBurnBody, NetworkGlobalID, to_nano from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2, get_wallet_address_get_method # Mnemonic phrase used to derive the wallet's private key MNEMONIC = "word1 word2 word3 ..." # Deployed jetton master contract address JETTON_MASTER_ADDRESS = Address("EQ...") # Amount of jettons to burn in base units JETTON_AMOUNT_TO_BURN = to_nano(1, decimals=9) async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must own jettons to burn successfully wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Get the owner's jetton wallet address via the master's get_wallet_address get-method jetton_wallet_address = await get_wallet_address_get_method( client=client, address=JETTON_MASTER_ADDRESS, owner_address=wallet.address, ) # Burn message body # response_address: address to receive excess TON and burn confirmation body = JettonBurnBody( jetton_amount=JETTON_AMOUNT_TO_BURN, response_address=wallet.address, ) # Send the burn message to the owner's jetton wallet (not the jetton master) msg = await wallet.transfer( destination=jetton_wallet_address, body=body.serialize(), amount=to_nano(0.05), ) print(f"Jetton wallet address: {jetton_wallet_address.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` Full runnable jetton example scripts on GitHub. # NFT Operations Source: https://tonutils.ness.uz/guide/examples/nft-operations Deploy Standard, Editable, and Soulbound NFT collections, mint and batch mint items, and manage content, editorship, and ownership with tonutils. This section covers working with NFT collections: **Standard**, **Editable**, and **Soulbound**. It includes collection deployment, minting, batch minting, content editing, and administrative operations such as changing collection ownership. Transferring an NFT to a new owner is a wallet operation. See [Send NFT](/guide/examples/wallet-operations#send-nft). ## Standard Collection A standard collection (TEP-62) mints immutable NFT items. Collection and item metadata use the TEP-64 off-chain format: the collection stores a base URI, and each item stores a suffix appended to it. ### Deploy Collection Compose the collection data from `NFTCollectionData` and deploy the contract by sending a message with its `state_init`. The collection address is derived deterministically from the code and data, so it is known before deployment. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import ( Address, NetworkGlobalID, NFTCollectionContent, NFTCollectionData, OffchainCommonContent, OffchainContent, RoyaltyParams, to_nano, ) from tonutils.clients import ToncenterClient from tonutils.contracts import NFTCollectionStandard, NFTItemStandard, WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # Collection owner address (controls collection) OWNER_ADDRESS = Address("UQ...") # Collection metadata URI (TEP-64 off-chain format) COLLECTION_URI = "https://example.com/collection.json" # Common prefix for individual NFT item metadata URIs # Full item URI = ITEMS_PREFIX_URI + item suffix (e.g., "0.json") ITEMS_PREFIX_URI = "https://example.com/items/" # Royalty fraction: numerator / denominator (50 / 1000 = 5%) ROYALTY = 50 ROYALTY_DENOMINATOR = 1000 # Address to receive royalty payments on secondary sales ROYALTY_ADDRESS = Address("UQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Get default NFT item code nft_item_code = NFTItemStandard.get_default_code() royalty_params = RoyaltyParams( royalty=ROYALTY, denominator=ROYALTY_DENOMINATOR, address=ROYALTY_ADDRESS, ) nft_collection_content = NFTCollectionContent( content=OffchainContent(uri=COLLECTION_URI), common_content=OffchainCommonContent(prefix_uri=ITEMS_PREFIX_URI), ) nft_collection_data = NFTCollectionData( owner_address=OWNER_ADDRESS, content=nft_collection_content, royalty_params=royalty_params, nft_item_code=nft_item_code, ) # Collection address is derived from code + data (predictable before deployment) nft_collection = NFTCollectionStandard.from_data( client=client, data=nft_collection_data.serialize(), ) # Deploy the collection via state_init message msg = await wallet.transfer( destination=nft_collection.address, amount=to_nano(0.05), state_init=nft_collection.state_init, ) print(f"NFT collection address: {nft_collection.address.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` **Parameters:** * `owner_address` — collection owner; only this address can mint items (`Address`). * `content` — collection-level metadata URI and the common prefix for item metadata URIs (`NFTCollectionContent`). * `royalty_params` — royalty share as `royalty / denominator` and the royalty destination address (`RoyaltyParams`). * `nft_item_code` — code cell used to deploy each NFT item (`Cell`). ### Mint NFT Minting sends an `NFTCollectionMintItemBody` message to the collection contract. The sending wallet must be the collection owner. The item address is calculated locally from the index, item code, and collection address. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import ( Address, NetworkGlobalID, NFTCollectionMintItemBody, NFTItemStandardMintRef, OffchainItemContent, to_nano, ) from tonutils.clients import ToncenterClient from tonutils.contracts import NFTCollectionStandard, NFTItemStandard, WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # NFT item owner address (receives minted NFT) OWNER_ADDRESS = Address("UQ...") # Deployed NFT collection contract address NFT_COLLECTION_ADDRESS = Address("EQ...") # Unique index for NFT item within collection (0, 1, 2, ...) NFT_ITEM_INDEX = 0 # Metadata suffix appended to collection's ITEMS_PREFIX_URI NFT_ITEM_SUFFIX_URI = "0.json" async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must be the collection owner to mint successfully wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) nft_item_code = NFTItemStandard.get_default_code() # Item initialization data: initial owner + metadata suffix 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), # Forwarded to the new item for storage rent ) msg = await wallet.transfer( destination=NFT_COLLECTION_ADDRESS, amount=to_nano(0.025), # Covers gas + forward_amount body=body.serialize(), ) # Item address is derived from: item_index + nft_item_code + collection_address 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() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` **Parameters:** * `item_index` — unique index within the collection; used for address calculation (`int`). * `item_ref` — serialized item initialization data: owner and content (`Cell`). * `forward_amount` — nanotons forwarded to the newly deployed item to cover initial storage fees; 0.01 TON is typical (`int`). ### Batch Mint NFT Batch minting deploys multiple items in a single transaction with `NFTCollectionBatchMintItemBody`. Items receive sequential indices starting from `from_index`. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import ( Address, NetworkGlobalID, NFTCollectionBatchMintItemBody, NFTItemStandardMintRef, OffchainItemContent, to_nano, ) from tonutils.clients import ToncenterClient from tonutils.contracts import NFTCollectionStandard, NFTItemStandard, WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # Starting index for batch minting MINT_FROM_INDEX = 0 # Owner addresses for batch-minted items; each address receives one NFT NFT_ITEM_OWNERS = [ Address("UQ..."), Address("UQ..."), Address("UQ..."), ] # Deployed NFT collection contract address NFT_COLLECTION_ADDRESS = Address("EQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must be the collection owner to mint successfully wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) nft_item_code = NFTItemStandard.get_default_code() nft_items_count = len(NFT_ITEM_OWNERS) nft_items_refs = [] # Build initialization data for each item with sequential indices for nft_item_index, owner_address in enumerate(NFT_ITEM_OWNERS, start=MINT_FROM_INDEX): nft_item_ref = NFTItemStandardMintRef( owner_address=owner_address, content=OffchainItemContent(suffix_uri=f"{nft_item_index}.json"), ) nft_items_refs.append(nft_item_ref.serialize()) body = NFTCollectionBatchMintItemBody( items_refs=nft_items_refs, from_index=MINT_FROM_INDEX, forward_amount=to_nano(0.01), # Forwarded to each newly deployed item ) msg = await wallet.transfer( destination=NFT_COLLECTION_ADDRESS, amount=to_nano(0.025) * nft_items_count, # Scales with batch size body=body.serialize(), ) for nft_item_index in range(MINT_FROM_INDEX, MINT_FROM_INDEX + nft_items_count): 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_index}): {nft_item_address.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` **Parameters:** * `items_refs` — list of serialized initialization data for all items (`list[Cell]`). * `from_index` — starting index; items get sequential indices from this value (`int`). * `forward_amount` — nanotons forwarded to each newly deployed item (`int`). ## Editable Collection An editable collection allows updating collection content, royalty parameters, and item metadata after deployment. Each item stores an editor address that is authorized to modify its content. ### Deploy Collection Deployment is identical to a standard collection; the only changes are the contract classes — `NFTCollectionEditable` and `NFTItemEditable`: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from tonutils.contracts import NFTCollectionEditable, NFTItemEditable, WalletV4R2 nft_item_code = NFTItemEditable.get_default_code() nft_collection = NFTCollectionEditable.from_data( client=client, data=nft_collection_data.serialize(), ) ``` ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import ( Address, NetworkGlobalID, NFTCollectionContent, NFTCollectionData, OffchainCommonContent, OffchainContent, RoyaltyParams, to_nano, ) from tonutils.clients import ToncenterClient from tonutils.contracts import NFTCollectionEditable, NFTItemEditable, WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # Collection owner address (can mint items and edit metadata) OWNER_ADDRESS = Address("UQ...") # Collection metadata URI (TEP-64 off-chain format) COLLECTION_URI = "https://example.com/collection.json" # Common prefix for individual NFT item metadata URIs ITEMS_PREFIX_URI = "https://example.com/items/" # Royalty fraction: numerator / denominator (50 / 1000 = 5%) ROYALTY = 50 ROYALTY_DENOMINATOR = 1000 # Address to receive royalty payments on secondary sales ROYALTY_ADDRESS = Address("UQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Get default Editable NFT item code nft_item_code = NFTItemEditable.get_default_code() royalty_params = RoyaltyParams( royalty=ROYALTY, denominator=ROYALTY_DENOMINATOR, address=ROYALTY_ADDRESS, ) nft_collection_content = NFTCollectionContent( content=OffchainContent(uri=COLLECTION_URI), common_content=OffchainCommonContent(prefix_uri=ITEMS_PREFIX_URI), ) nft_collection_data = NFTCollectionData( owner_address=OWNER_ADDRESS, content=nft_collection_content, royalty_params=royalty_params, nft_item_code=nft_item_code, ) nft_collection = NFTCollectionEditable.from_data( client=client, data=nft_collection_data.serialize(), ) msg = await wallet.transfer( destination=nft_collection.address, amount=to_nano(0.05), state_init=nft_collection.state_init, ) print(f"NFT collection address: {nft_collection.address.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Mint NFT Editable items use `NFTItemEditableMintRef`, which adds an `editor_address` authorized to modify the item metadata after minting. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import ( Address, NetworkGlobalID, NFTCollectionMintItemBody, NFTItemEditableMintRef, OffchainItemContent, to_nano, ) from tonutils.clients import ToncenterClient from tonutils.contracts import NFTCollectionEditable, NFTItemEditable, WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # NFT item owner address (receives minted Editable NFT) OWNER_ADDRESS = Address("UQ...") # Editor address (can modify NFT metadata after minting) EDITOR_ADDRESS = Address("UQ...") # Deployed NFT collection contract address NFT_COLLECTION_ADDRESS = Address("EQ...") # Unique index for NFT item within collection (0, 1, 2, ...) NFT_ITEM_INDEX = 0 # Metadata suffix appended to collection's ITEMS_PREFIX_URI NFT_ITEM_SUFFIX_URI = f"{NFT_ITEM_INDEX}.json" async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must be the collection owner to mint successfully wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) nft_item_code = NFTItemEditable.get_default_code() # Item initialization data: initial owner + editor + metadata suffix nft_item_ref = NFTItemEditableMintRef( owner_address=OWNER_ADDRESS, editor_address=EDITOR_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 = NFTCollectionEditable.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() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Batch Mint NFT Batch minting works the same as in a standard collection; the only changes are the Editable classes and `NFTItemEditableMintRef`, which sets an `editor_address` for each item: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NFTItemEditableMintRef from tonutils.contracts import NFTCollectionEditable, NFTItemEditable nft_item_ref = NFTItemEditableMintRef( owner_address=owner_address, editor_address=editor_address, content=OffchainItemContent(suffix_uri=f"{nft_item_index}.json"), ) ``` ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import ( Address, NetworkGlobalID, NFTCollectionBatchMintItemBody, NFTItemEditableMintRef, OffchainItemContent, to_nano, ) from tonutils.clients import ToncenterClient from tonutils.contracts import NFTCollectionEditable, NFTItemEditable, WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # Starting index for batch minting MINT_FROM_INDEX = 0 # (owner_address, editor_address) pairs; each pair produces one item NFT_ITEM_OWNERS_AND_EDITORS = [ (Address("UQ..."), Address("UQ...")), (Address("UQ..."), Address("UQ...")), (Address("UQ..."), Address("UQ...")), ] # Deployed NFT collection contract address NFT_COLLECTION_ADDRESS = Address("EQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must be the collection owner to mint successfully wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) nft_item_code = NFTItemEditable.get_default_code() nft_items_count = len(NFT_ITEM_OWNERS_AND_EDITORS) nft_items_refs = [] for nft_item_index, (owner_address, editor_address) in enumerate( NFT_ITEM_OWNERS_AND_EDITORS, start=MINT_FROM_INDEX ): nft_item_ref = NFTItemEditableMintRef( owner_address=owner_address, editor_address=editor_address, content=OffchainItemContent(suffix_uri=f"{nft_item_index}.json"), ) nft_items_refs.append(nft_item_ref.serialize()) body = NFTCollectionBatchMintItemBody( items_refs=nft_items_refs, from_index=MINT_FROM_INDEX, forward_amount=to_nano(0.01), ) msg = await wallet.transfer( destination=NFT_COLLECTION_ADDRESS, amount=to_nano(0.025) * nft_items_count, body=body.serialize(), ) for nft_item_index in range(MINT_FROM_INDEX, MINT_FROM_INDEX + nft_items_count): nft_item_address = NFTCollectionEditable.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_index}): {nft_item_address.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Edit NFT Content Send an `NFTEditContentBody` message to the NFT item contract. The sending wallet must be the item's editor. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import ( Address, NetworkGlobalID, NFTEditContentBody, OffchainItemContent, to_nano, ) from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # Editable NFT item address to update NFT_ITEM_ADDRESS = Address("EQ...") # New metadata suffix appended to collection's ITEMS_PREFIX_URI SUFFIX_URI = "0.json" async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must be the item's editor to edit content successfully wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) body = NFTEditContentBody( content=OffchainItemContent(suffix_uri=SUFFIX_URI), ) msg = await wallet.transfer( destination=NFT_ITEM_ADDRESS, body=body.serialize(), amount=to_nano(0.05), ) print(f"NFT item address: {NFT_ITEM_ADDRESS.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Change NFT Editorship Send an `NFTTransferEditorshipBody` message to the NFT item contract. The sending wallet must be the current editor. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, NetworkGlobalID, NFTTransferEditorshipBody, to_nano from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # Editable NFT item address to transfer editorship NFT_ITEM_ADDRESS = Address("EQ...") # New editor address EDITOR_ADDRESS = Address("UQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must be the current editor to transfer editorship successfully wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) body = NFTTransferEditorshipBody( editor_address=EDITOR_ADDRESS, response_address=wallet.address, # Receives excess TON ) msg = await wallet.transfer( destination=NFT_ITEM_ADDRESS, body=body.serialize(), amount=to_nano(0.05), ) print(f"NFT item address: {NFT_ITEM_ADDRESS.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Edit Collection Content Send an `NFTCollectionChangeContentBody` message to the collection contract to replace the collection metadata and royalty parameters. The sending wallet must be the collection owner. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import ( Address, NetworkGlobalID, NFTCollectionChangeContentBody, NFTCollectionContent, OffchainCommonContent, OffchainContent, RoyaltyParams, to_nano, ) from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # Deployed NFT collection contract address to update NFT_COLLECTION_ADDRESS = Address("EQ...") # New collection metadata URI (TEP-64 off-chain format) COLLECTION_URI = "https://example.com/collection.json" # New common prefix for individual NFT item metadata URIs ITEMS_PREFIX_URI = "https://example.com/items/" # Royalty fraction: numerator / denominator (50 / 1000 = 5%) ROYALTY = 50 ROYALTY_DENOMINATOR = 1000 # New address to receive royalty payments on secondary sales ROYALTY_ADDRESS = Address("UQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must be the collection owner to change content successfully wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) royalty_params = RoyaltyParams( royalty=ROYALTY, denominator=ROYALTY_DENOMINATOR, address=ROYALTY_ADDRESS, ) nft_collection_content = NFTCollectionContent( content=OffchainContent(uri=COLLECTION_URI), common_content=OffchainCommonContent(prefix_uri=ITEMS_PREFIX_URI), ) body = NFTCollectionChangeContentBody( content=nft_collection_content, royalty_params=royalty_params, ) msg = await wallet.transfer( destination=NFT_COLLECTION_ADDRESS, body=body.serialize(), amount=to_nano(0.05), ) print(f"NFT collection address: {NFT_COLLECTION_ADDRESS.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Change Collection Owner Send an `NFTCollectionChangeOwnerBody` message to the collection contract. The sending wallet must be the current owner. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, NetworkGlobalID, NFTCollectionChangeOwnerBody, to_nano from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # New owner address OWNER_ADDRESS = Address("UQ...") # Deployed NFT collection contract address to transfer NFT_COLLECTION_ADDRESS = Address("EQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must be the current owner to change ownership successfully wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) body = NFTCollectionChangeOwnerBody(owner_address=OWNER_ADDRESS) msg = await wallet.transfer( destination=NFT_COLLECTION_ADDRESS, body=body.serialize(), amount=to_nano(0.05), ) print(f"NFT collection address: {NFT_COLLECTION_ADDRESS.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ## Soulbound Collection Soulbound tokens (SBT, TEP-85) are non-transferable NFTs bound to their owner. Each item stores an authority address that can revoke it; the owner can destroy it. There is no separate soulbound collection class. Use `NFTCollectionStandard` with the soulbound item code from `NFTItemSoulbound.get_default_code()`. ### Deploy Collection Deployment is identical to a standard collection; the only change is that the item code comes from `NFTItemSoulbound`: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from tonutils.contracts import NFTCollectionStandard, NFTItemSoulbound, WalletV4R2 nft_item_code = NFTItemSoulbound.get_default_code() ``` ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import ( Address, NetworkGlobalID, NFTCollectionContent, NFTCollectionData, OffchainCommonContent, OffchainContent, RoyaltyParams, to_nano, ) from tonutils.clients import ToncenterClient from tonutils.contracts import NFTCollectionStandard, NFTItemSoulbound, WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # Collection owner address (controls collection) OWNER_ADDRESS = Address("UQ...") # Collection metadata URI (TEP-64 off-chain format) COLLECTION_URI = "https://example.com/collection.json" # Common prefix for individual NFT item metadata URIs ITEMS_PREFIX_URI = "https://example.com/items/" # Royalty fraction: numerator / denominator (50 / 1000 = 5%) ROYALTY = 50 ROYALTY_DENOMINATOR = 1000 # Address to receive royalty payments # Required by TEP-66, even though SBT are non-transferable per TEP-85 ROYALTY_ADDRESS = Address("UQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Get default Soulbound NFT item code nft_item_code = NFTItemSoulbound.get_default_code() royalty_params = RoyaltyParams( royalty=ROYALTY, denominator=ROYALTY_DENOMINATOR, address=ROYALTY_ADDRESS, ) nft_collection_content = NFTCollectionContent( content=OffchainContent(uri=COLLECTION_URI), common_content=OffchainCommonContent(prefix_uri=ITEMS_PREFIX_URI), ) nft_collection_data = NFTCollectionData( owner_address=OWNER_ADDRESS, content=nft_collection_content, royalty_params=royalty_params, nft_item_code=nft_item_code, ) nft_collection = NFTCollectionStandard.from_data( client=client, data=nft_collection_data.serialize(), ) msg = await wallet.transfer( destination=nft_collection.address, amount=to_nano(0.05), state_init=nft_collection.state_init, ) print(f"NFT collection address: {nft_collection.address.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Mint NFT Soulbound items use `NFTItemSoulboundMintRef`, which adds an `authority_address` that can later revoke the item. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import ( Address, NetworkGlobalID, NFTCollectionMintItemBody, NFTItemSoulboundMintRef, OffchainItemContent, to_nano, ) from tonutils.clients import ToncenterClient from tonutils.contracts import NFTCollectionStandard, NFTItemSoulbound, WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # NFT item owner address (receives minted Soulbound NFT) OWNER_ADDRESS = Address("UQ...") # Authority address (can revoke the Soulbound NFT) AUTHORITY_ADDRESS = Address("UQ...") # Deployed NFT collection contract address NFT_COLLECTION_ADDRESS = Address("EQ...") # Unique index for NFT item within collection (0, 1, 2, ...) NFT_ITEM_INDEX = 0 # Metadata suffix appended to collection's ITEMS_PREFIX_URI NFT_ITEM_SUFFIX_URI = "0.json" async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must be the collection owner to mint successfully wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) nft_item_code = NFTItemSoulbound.get_default_code() # Item initialization data: initial owner + authority + metadata suffix nft_item_ref = NFTItemSoulboundMintRef( owner_address=OWNER_ADDRESS, authority_address=AUTHORITY_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() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Batch Mint NFT Batch minting works the same as in a standard collection; the only changes are the Soulbound item code and `NFTItemSoulboundMintRef`, which sets an `authority_address` for each item: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NFTItemSoulboundMintRef from tonutils.contracts import NFTCollectionStandard, NFTItemSoulbound nft_item_ref = NFTItemSoulboundMintRef( owner_address=owner_address, authority_address=authority_address, content=OffchainItemContent(suffix_uri=f"{nft_item_index}.json"), ) ``` ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import ( Address, NetworkGlobalID, NFTCollectionBatchMintItemBody, NFTItemSoulboundMintRef, OffchainItemContent, to_nano, ) from tonutils.clients import ToncenterClient from tonutils.contracts import NFTCollectionStandard, NFTItemSoulbound, WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # Starting index for batch minting MINT_FROM_INDEX = 0 # (owner_address, authority_address) pairs; each pair produces one item NFT_ITEM_OWNERS_AND_AUTHORITIES = [ (Address("UQ..."), Address("UQ...")), (Address("UQ..."), Address("UQ...")), (Address("UQ..."), Address("UQ...")), ] # Deployed NFT collection contract address NFT_COLLECTION_ADDRESS = Address("EQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must be the collection owner to mint successfully wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) nft_item_code = NFTItemSoulbound.get_default_code() nft_items_count = len(NFT_ITEM_OWNERS_AND_AUTHORITIES) nft_items_refs = [] for nft_item_index, (owner_address, authority_address) in enumerate( NFT_ITEM_OWNERS_AND_AUTHORITIES, start=MINT_FROM_INDEX ): nft_item_ref = NFTItemSoulboundMintRef( owner_address=owner_address, authority_address=authority_address, content=OffchainItemContent(suffix_uri=f"{nft_item_index}.json"), ) nft_items_refs.append(nft_item_ref.serialize()) body = NFTCollectionBatchMintItemBody( items_refs=nft_items_refs, from_index=MINT_FROM_INDEX, forward_amount=to_nano(0.01), ) msg = await wallet.transfer( destination=NFT_COLLECTION_ADDRESS, amount=to_nano(0.025) * nft_items_count, body=body.serialize(), ) for nft_item_index in range(MINT_FROM_INDEX, MINT_FROM_INDEX + nft_items_count): 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_index}): {nft_item_address.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Revoke NFT Send an `NFTRevokeBody` message to the SBT item contract. Per TEP-85, only the item's authority can revoke it; revocation sets the on-chain revoked timestamp. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, NetworkGlobalID, NFTRevokeBody, to_nano from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # NFT item address to revoke NFT_ITEM_ADDRESS = Address("EQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must be the NFT authority to revoke successfully wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) body = NFTRevokeBody() msg = await wallet.transfer( destination=NFT_ITEM_ADDRESS, body=body.serialize(), amount=to_nano(0.05), ) print(f"NFT item address: {NFT_ITEM_ADDRESS.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Destroy NFT Send an `NFTDestroyBody` message to the SBT item contract. Per TEP-85, only the item's owner can destroy it. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, NetworkGlobalID, NFTDestroyBody, to_nano from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # NFT item address to destroy NFT_ITEM_ADDRESS = Address("EQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must be the NFT owner to destroy successfully (per TEP-85) wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) body = NFTDestroyBody() msg = await wallet.transfer( destination=NFT_ITEM_ADDRESS, body=body.serialize(), amount=to_nano(0.05), ) print(f"NFT item address: {NFT_ITEM_ADDRESS.to_str()}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` Full runnable scripts for all NFT operations on GitHub. # Wallet Operations Source: https://tonutils.ness.uz/guide/examples/wallet-operations Create, import, deploy, and inspect wallets, and send TON, jettons, and NFTs with tonutils. This section provides a complete guide to managing wallets using the `tonutils` library. It covers key operations such as: * Creating and deploying wallets * Importing wallets from mnemonic or private key * Reading on-chain wallet information * Sending transactions (TON, NFTs, jettons) * Performing batch and sequential transfers * Sending gasless (relayed) jetton transfers * Rotating the signing key of a Telegram wallet (WalletTg) * Encrypting and decrypting on-chain messages ## Supported Wallets The library supports multiple wallet versions and types, all importable from `tonutils.contracts`: | Class | Type | Max messages | Recommended for | | - | - | - | - | | `WalletV1R1` | Standard | 1 | — | | `WalletV1R2` | Standard | 1 | — | | `WalletV1R3` | Standard | 1 | — | | `WalletV2R1` | Standard | 4 | — | | `WalletV2R2` | Standard | 4 | — | | `WalletV3R1` | Standard | 4 | — | | `WalletV3R2` | Standard | 4 | General use | | `WalletV4R1` | Standard | 4 | — | | `WalletV4R2` | Standard | 4 | General use | | `WalletV5Beta` | Standard | 255 | — | | `WalletV5R1` | Standard | 255 | General use (preferred): full feature support, including gasless transfers | | `WalletHighloadV2` | Highload | 254 | — | | `WalletHighloadV3R1` | Highload | 64516 (254 × 254) | Services and exchanges | | `WalletPreprocessedV2` | Preprocessed | 255 | Large-scale batch transfers where gas optimization is critical | | `WalletTg` | Telegram | 255 | Telegram-created wallets with on-chain signing-key rotation | ## Create Wallet To create a new wallet, use the `.create()` classmethod of the wallet class you select. It returns a 4-tuple: the wallet instance, its **public key**, **private key**, and a freshly generated **mnemonic** phrase. Each wallet class takes an optional `config=` argument (`WalletV4Config`, `WalletV5Config`, etc. from `ton_core`), where you can customize `subwallet_id` to derive multiple wallets from the same mnemonic. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID, WalletV4Config from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 def main() -> None: # Initialize HTTP client for TON blockchain interaction # NetworkGlobalID.MAINNET (-239) for production # NetworkGlobalID.TESTNET (-3) for testing client = ToncenterClient(network=NetworkGlobalID.MAINNET) # Wallet configuration (default settings) # Can customize subwallet_id for multiple wallets from same mnemonic config = WalletV4Config() # Create new wallet with fresh mnemonic and keypair # Returns: (wallet, public_key, private_key, mnemonic) wallet, public_key, private_key, mnemonic = WalletV4R2.create(client, config=config) # Get wallet address in user-friendly format # is_bounceable=False: standard for wallet contracts (UQ...) address = wallet.address.to_str(is_bounceable=False) # Output wallet credentials print(f"Address: {address}") print(f"Mnemonic: {' '.join(mnemonic)}") print(f"Public Key: {public_key.as_int}") print(f"Private Key: {private_key.as_b64}") print(f"Keypair: {private_key.keypair.as_b64}") if __name__ == "__main__": main() ``` To use a different wallet version, swap the class and its config together: for example `WalletV5R1` with `WalletV5Config`, or `WalletHighloadV3R1` with `WalletHighloadV3Config`. Configs are imported from `ton_core`. ## Import Wallet You can import a wallet either from a **mnemonic** phrase or directly from a **private key**. `from_mnemonic()` returns the same 4-tuple as `.create()`; `from_private_key()` returns only the wallet instance. ### From Mnemonic Mnemonics can be 24 words (TON-native) or 12/18/24 words (BIP-39 import). ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID, WalletV4Config from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) # Wallet configuration (default settings) config = WalletV4Config() # Create wallet from mnemonic phrase # Returns: (wallet, public_key, private_key, mnemonic) wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC, config=config) # is_bounceable=False: standard for wallet contracts (UQ...) print(f"Wallet address: {wallet.address.to_str(is_bounceable=False)}") if __name__ == "__main__": main() ``` ### From Private Key `PrivateKey` from `ton_core` accepts the key in multiple formats: raw `bytes`, a hex string, a base64 string, or an integer. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID, PrivateKey, WalletV4Config from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Private key can be provided in multiple formats: # - bytes: raw binary representation # - hex string: hexadecimal encoding # - base64 string: base64 encoding # - integer: numeric representation PRIVATE_KEY = PrivateKey("") def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) # Wallet configuration (default settings) config = WalletV4Config() # Create wallet from existing private key wallet = WalletV4R2.from_private_key(client, PRIVATE_KEY, config=config) # is_bounceable=False: standard for wallet contracts (UQ...) print(f"Wallet address: {wallet.address.to_str(is_bounceable=False)}") if __name__ == "__main__": main() ``` ## Deploy Wallet Wallets deploy automatically on their first outgoing transaction. To deploy explicitly, reconstruct the wallet from a mnemonic and send a small amount to its own address — after the transaction, the wallet transitions from `uninit` to `active` status. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID, to_nano from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Create wallet instance from mnemonic # Returns: (wallet, public_key, private_key, mnemonic) wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Deploy wallet by sending any transaction # Sending 0.05 TON to self deploys the wallet contract # Wallet auto-deploys on first outgoing transaction if in uninit status msg = await wallet.transfer(destination=wallet.address, amount=to_nano(0.05)) print(f"Wallet address: {wallet.address.to_str(is_bounceable=False)}") print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` `wallet.transfer(...)` returns an `ExternalMessage`. Its `normalized_hash` is computed locally before sending — it is **not** the on-chain transaction hash. Use it to track whether the message was accepted on-chain via explorers or API queries. ## Wallet Information To inspect a wallet without signing anything, initialize it from an address with `.from_address()` (read-only mode). Call `await wallet.refresh()` to fetch the latest on-chain state before reading properties. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, NetworkGlobalID, to_amount from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Address of the wallet you want to inspect WALLET_ADDRESS = Address("UQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Initialize wallet from existing address (read-only mode) # Use this when you only need to query wallet state without signing wallet = await WalletV4R2.from_address(client, WALLET_ADDRESS) # Fetch latest on-chain state from blockchain # Updates: balance, state, last_transaction_lt, last_transaction_hash await wallet.refresh() # Convert balance from nanotons to TON with 4 decimal precision ton_balance = to_amount(wallet.balance, precision=4) print(f"Address: {wallet.address.to_str(is_bounceable=False)}") # State: nonexist, uninit (not deployed), active (deployed), frozen print(f"State: {wallet.state.value}") # Balance in nanotons and human-readable TON format print(f"Balance: {wallet.balance} ({ton_balance} TON)") # Logical time and hash of the last transaction print(f"Last transaction lt: {wallet.last_transaction_lt}") print(f"Last transaction hash: {wallet.last_transaction_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` If you also need to send transactions, initialize the wallet from a mnemonic instead — `.refresh()` and the state properties work the same way. For arbitrary contracts, see [Get Contract Information](/how-to/get-contract-information). ## Transfers ### Send TON Use `wallet.transfer()` to send TON with an optional text comment. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, NetworkGlobalID, to_nano from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # Destination address (recipient) DESTINATION_ADDRESS = Address("UQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Create wallet instance from mnemonic (full access mode) wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Send TON with optional text comment msg = await wallet.transfer( destination=DESTINATION_ADDRESS, amount=to_nano(0.01), # Convert 0.01 TON to nanotons (10,000,000) body="Hello from tonutils!", ) print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` **Parameters:** * `destination` — recipient address (`Address`). * `amount` — amount in nanotons; use `to_nano()` to convert from TON (`int`). * `body` — optional text comment, visible in explorers and the recipient wallet (`str` or `Cell`). ### Send NFT NFT transfers use `NFTTransferBuilder` with `wallet.transfer_message()`. The sending wallet must be the current owner of the NFT. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, NetworkGlobalID, to_nano from tonutils.clients import ToncenterClient from tonutils.contracts import NFTTransferBuilder, WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # NFT item address (specific NFT token) NFT_ITEM_ADDRESS = Address("EQ...") # Destination address (new NFT owner) DESTINATION_ADDRESS = Address("UQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must be the current owner of the NFT wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Transfer NFT to new owner msg = await wallet.transfer_message( NFTTransferBuilder( destination=DESTINATION_ADDRESS, nft_address=NFT_ITEM_ADDRESS, forward_payload="Hello from tonutils!", forward_amount=1, # 1 nanoton triggers ownership_assigned notification amount=to_nano(0.05), # 0.05 TON for gas (covers NFT transfer fees) ) ) print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` **Parameters:** * `destination` — new owner address (`Address`). * `nft_address` — NFT item contract address (`Address`). * `forward_payload` — optional message forwarded to the new owner, visible in the notification (`str` or `Cell`). * `forward_amount` — nanotons sent to the new owner with the `ownership_assigned` notification (TEP-62); must be greater than 0 to trigger the notification (`int`). * `amount` — TON attached to the NFT item for gas fees; 0.05 TON is typically sufficient, increase it if `forward_amount` is higher (`int`). ### Send Jetton Jetton transfers use `JettonTransferBuilder` with `wallet.transfer_message()`. The wallet must hold a jetton balance in its jetton wallet. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, NetworkGlobalID, to_nano from tonutils.clients import ToncenterClient from tonutils.contracts import JettonTransferBuilder, WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # Destination address (jetton recipient) DESTINATION_ADDRESS = Address("UQ...") # Jetton master contract address (identifies the token type) JETTON_MASTER_ADDRESS = Address("EQ...") # Jetton amount in base units (respects token decimals) # Example: 1 USDT (6 decimals) -> 1 * 10^6 = 1,000,000 JETTON_AMOUNT_TO_SEND = to_nano(1, decimals=6) async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Wallet must have jetton balance in its jetton wallet wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Transfer jettons to recipient msg = await wallet.transfer_message( JettonTransferBuilder( destination=DESTINATION_ADDRESS, jetton_amount=JETTON_AMOUNT_TO_SEND, jetton_master_address=JETTON_MASTER_ADDRESS, forward_payload="Hello from tonutils!", forward_amount=1, # 1 nanoton triggers transfer_notification amount=to_nano(0.05), # 0.05 TON for gas (covers jetton transfer fees) ) ) print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` **Parameters:** * `destination` — recipient address that receives the jettons (`Address`). * `jetton_amount` — amount in base units, respecting the token's decimals (`int`). * `jetton_master_address` — jetton master contract address identifying the token (`Address`). * `forward_payload` — optional message forwarded to the recipient, visible in the notification (`str` or `Cell`). * `forward_amount` — nanotons sent to the recipient with the `transfer_notification` (TEP-74); must be greater than 0 to trigger the notification (`int`). * `amount` — TON attached to the jetton wallet for gas fees; 0.05 TON is typically sufficient (`int`). Jetton amounts respect the token's decimals, which are not always 9. For USDT-like jettons with 6 decimals, convert with `to_nano(1, decimals=6)`. ## Batch Transfers Send multiple transfers in a single transaction by passing a list of builders to `wallet.batch_transfer_message()`. You can mix `TONTransferBuilder`, `NFTTransferBuilder`, and `JettonTransferBuilder` in one batch. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, NetworkGlobalID, to_nano from tonutils.clients import ToncenterClient from tonutils.contracts import TONTransferBuilder, WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() wallet, _, _, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Send multiple transfers in a single transaction (batch transfer) # Can mix TONTransferBuilder, NFTTransferBuilder, and JettonTransferBuilder msg = await wallet.batch_transfer_message( [ TONTransferBuilder( destination=Address("UQ..."), amount=to_nano(0.01), body="Hello from tonutils!", ), TONTransferBuilder( destination=Address("UQ..."), amount=to_nano(0.01), body="Hello from tonutils!", ), TONTransferBuilder( destination=Address("UQ..."), amount=to_nano(0.01), body="Hello from tonutils!", ), ] ) print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` Message limits per wallet version are listed in [Supported Wallets](#supported-wallets); exceeding the limit raises an error. ## Sequential Transfers Standard wallets reject a transaction whose seqno has already been used. If you send several transfers in quick succession, the second one can reuse the same on-chain seqno and fail. Wrap the wallet in `SeqnoGuard` to send safely in sequence — it waits for the on-chain seqno to advance after each send before allowing the next. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, NetworkGlobalID, to_nano from tonutils.clients import ToncenterClient from tonutils.contracts import SeqnoGuard, TONTransferBuilder, WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # Destination addresses (recipients) 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 # timeout: max seconds to wait for seqno to advance (default: 30.0) # poll_interval: delay between seqno polls in seconds (default: 1.5) guard = SeqnoGuard(wallet, timeout=30.0, poll_interval=1.0) # Send TON transfers sequentially — each confirmed before the next # guard.transfer() has the same signature as wallet.transfer() 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}") # guard.transfer_message() has the same signature as wallet.transfer_message() # Accepts any message builder: TONTransferBuilder, JettonTransferBuilder, NFTTransferBuilder 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()) ``` ## Gasless Transfers Gasless transfers let a wallet send jettons without holding TON for gas: a relay pays the TON fees, and its commission is deducted from the sender's jetton balance. The flow is two steps — `gasless_estimate()` to build and price the transfer, then `gasless_send()` to sign and relay it. Gasless transfers require **`TonapiClient` on mainnet** and are only available for **WalletV5** (`WalletV5R1` and `WalletV5Beta`). ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, NetworkGlobalID, to_nano from tonutils.clients import TonapiClient from tonutils.contracts import WalletV5R1 # Tonapi API key (required for gasless transfers) API_KEY = "YOUR_API_KEY" # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # Jetton master contract address (identifies the token type) # This jetton is used both for the transfer and for gas payment # Supported jettons: check https://tonapi.io/v2/gasless/config via Tonapi JETTON_MASTER_ADDRESS = Address("EQ...") # Destination address (jetton recipient) DESTINATION_ADDRESS = Address("UQ...") # Jetton amount in base units (respects token decimals) # Example: 1 USDT (6 decimals) -> 1 * 10^6 = 1,000,000 JETTON_AMOUNT_TO_SEND = to_nano(1, decimals=6) async def main() -> None: # Gasless transfers are only supported via TonapiClient on MAINNET client = TonapiClient(network=NetworkGlobalID.MAINNET, api_key=API_KEY) await client.connect() # Gasless transfers are only available for WalletV5 (R1 and Beta) wallet, _, _, _ = WalletV5R1.from_mnemonic(client, MNEMONIC) # Step 1: Estimate the gasless transfer # Parameters mirror JettonTransferBuilder estimate = await wallet.gasless_estimate( destination=DESTINATION_ADDRESS, jetton_amount=JETTON_AMOUNT_TO_SEND, jetton_master_address=JETTON_MASTER_ADDRESS, ) # Step 2: Review the estimation result before sending # estimate.commission — relay fee in nanocoins (deducted from jetton balance) # estimate.relay_address — address of the relay paying gas # estimate.valid_until — unix timestamp, transaction expires after this time # estimate.messages — messages to sign (built by the relay) # estimate.emulation — full transaction emulation with trace and risk analysis print(f"Commission: {estimate.commission} nanocoins") print(f"Valid until: {estimate.valid_until}") # Step 3: Sign and send via gasless relay await wallet.gasless_send(estimate) print("Gasless transfer sent!") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` A Tonapi API key is required — get one at [tonconsole.com](https://tonconsole.com/). Always review `estimate.commission`, `estimate.valid_until`, and the emulation result before calling `gasless_send()`; once sent, the transaction is irreversible. The same flow is also available as a standalone recipe: [Send Gasless Transaction](/how-to/send-gasless-transaction). ## Telegram Wallet (WalletTg) `WalletTg` is the wallet contract used by Telegram. Its account code is a small immutable trampoline that runs shared logic stored in the blockchain config, so every `WalletTg` uses the same latest contract version, updated by validator voting. It supports up to 255 messages per transaction and adds one capability the standard wallets do not have: rotating its signing key on-chain. Its default `subwallet_id` is network-specific and resolved automatically from the client's network. ### Rotate Signing Key `change_public_key_from_mnemonic()` replaces the wallet's on-chain signing key. The request is signed twice — by the current key, which authorizes the change, and by the new key, which proves ownership of the key being installed. The request also carries the old key encrypted with the new one, so the holder of the new key can still decrypt comments sent to the old one. The wallet **address does not change**: the balance and history stay in place, only the key that controls them changes. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID from tonutils.clients import ToncenterClient from tonutils.contracts import WalletTg # Current mnemonic controlling the wallet MNEMONIC = "word1 word2 word3 ..." # New mnemonic to rotate the signing key to NEW_MNEMONIC = "word1 word2 word3 ..." async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Open the wallet with its current mnemonic wallet, _, _, _ = WalletTg.from_mnemonic(client, MNEMONIC) # Rotate the signing key to NEW_MNEMONIC # Returns: (external_message, public_key, private_key, mnemonic) msg, _, _, _ = await wallet.change_public_key_from_mnemonic(NEW_MNEMONIC) print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` After the rotation is accepted on-chain, the old mnemonic no longer controls the wallet. Reopen it with the new mnemonic using `from_address_and_mnemonic()` — `from_mnemonic()` would derive a different, empty address. The old key is encrypted with the new one and a key-change salt, `WALLET_TG_KEY_CHANGE_SALT` by default. Recover it from the rotation's external-out body with `WalletTgKeyChangedBody.decrypt()`. A custom `salt` passed to the rotation methods is known only to you: clients using the default salt cannot recover the old key, and losing the salt loses it for good. ### Open by Address After a key rotation, the wallet's address no longer derives from its mnemonic. Open it by its known address instead: `from_address_and_mnemonic()` binds the current key to the address and verifies the key actually controls it. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, NetworkGlobalID from tonutils.clients import ToncenterClient from tonutils.contracts import WalletTg # Address of the existing wallet to open WALLET_ADDRESS = Address("UQ...") # Current mnemonic controlling that wallet MNEMONIC = "word1 word2 word3 ..." async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Open the wallet by address with the current mnemonic # Verifies the key controls the address before returning # Returns: (wallet, public_key, private_key, mnemonic) wallet, _, _, _ = await WalletTg.from_address_and_mnemonic(client, WALLET_ADDRESS, MNEMONIC) print(f"Wallet address: {wallet.address.to_str(is_bounceable=False)}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` `from_address_and_private_key()` is the private-key equivalent of `from_address_and_mnemonic()`, and `change_public_key()` rotates to a `PrivateKey` directly instead of a mnemonic. ## Encrypted Messages The library supports end-to-end encrypted comments using elliptic curve Diffie-Hellman (ECDH): only the sender and the recipient can decrypt the message. ### Encrypt Fetch the recipient's public key with `get_public_key_get_method()`, encrypt the text with `TextCipher.encrypt()`, and send the resulting body with a regular transfer. The recipient's wallet must be deployed (`active` state) and expose the `get_public_key` get-method. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} 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 phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # Destination address (recipient) # Recipient must have a deployed wallet with an accessible public key DESTINATION_ADDRESS = Address("UQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Keep private_key for encryption as our_private_key wallet, _, our_private_key, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Retrieve recipient's public key from blockchain # Calls get_public_key() get-method on recipient's wallet contract their_public_key = await get_public_key_get_method( client=client, address=DESTINATION_ADDRESS, ) # Encrypt message using end-to-end encryption (ECDH) body = TextCipher.encrypt( payload="Hello from tonutils!", sender_address=wallet.address, our_private_key=our_private_key, their_public_key=their_public_key, ) # Send TON with encrypted message body msg = await wallet.transfer( destination=DESTINATION_ADDRESS, amount=to_nano(0.01), body=body, ) print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` **Parameters:** * `payload` — plaintext message to encrypt (`str`). * `sender_address` — sender's address, used as a salt for message-key derivation and verification; the recipient must pass the same address to decrypt (`Address`). * `our_private_key` — sender's private key for ECDH (`PrivateKey`). * `their_public_key` — recipient's public key for ECDH (`PublicKey`). ### Decrypt Decrypt an incoming encrypted comment with `TextCipher.decrypt()`. The sender's public key is recovered from the encrypted payload (it is stored XORed with the recipient's public key), so only the sender's address and the recipient's private key are needed. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, NetworkGlobalID, TextCipher from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Mnemonic phrase — 24 words (TON-native) or 12/18/24 words (BIP-39 import) MNEMONIC = "word1 word2 word3 ..." # Sender address (original message sender) # Used for verification during decryption SENDER_ADDRESS = Address("UQ...") # Encrypted payload from transaction body (opcode 0x2167DA4B) # Can be provided in multiple formats: # - bytes / hex string / base64 string: cipher text payload only # - Cell: full message body from BOC (opcode + cipher text payload) ENCRYPTED_PAYLOAD = "" async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) # Keep private_key for decryption as our_private_key _, _, our_private_key, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Decrypt message using end-to-end encryption (ECDH) decrypted_comment = TextCipher.decrypt( payload=ENCRYPTED_PAYLOAD, sender_address=SENDER_ADDRESS, our_private_key=our_private_key, ) print(f"Decrypted message: {decrypted_comment}") if __name__ == "__main__": import asyncio asyncio.run(main()) ``` **Parameters:** * `payload` — encrypted message as `bytes`, hex string, base64 string (cipher text only), or a `Cell` containing the opcode `0x2167DA4B` and cipher text. * `sender_address` — original sender's address, used for verification (`Address`). * `our_private_key` — recipient's private key for ECDH decryption (`PrivateKey`). For more details, see [Send Encrypted Message](/how-to/send-encrypted-message). Full runnable wallet example scripts on GitHub. # Getting Testnet Assets Source: https://tonutils.ness.uz/guide/getting-testnet-assets Claim testnet TON, NOT, and USD₮ for development and testing. This section explains how to claim testnet assets. ## TON TON QR To get TON, go to the Telegram bot and follow its instructions. [**Open Telegram Bot**](https://t.me/testgiver_ton_bot) ## NOT Jetton (9 Decimals) NOT QR To claim NOT jettons, scan this QR code in your TON wallet. [**Claim Testnet NOT**](ton://transfer/kQBMfIaxfLQMP4h1Pg2V_AuyToC3jdB8MmA6u3bx8i1__NOT?amount=100000000\&bin=te6ccgEBAgEAKgABIWQrfQcAAAAAAAAAABAX14QCAQAnF41FGQAAAAAAAAAAXo1KUQAAEBQ%3D) ## USD₮ Jetton (6 Decimals) USD₮ QR To claim USD₮ jettons, scan this QR code in your TON wallet. [**Claim Testnet USD₮**](ton://transfer/kQB0ZYUL5M3KfrW0tSnwdFO1nC-BQHC2gcZl-WaF2on_USDT?amount=100000000\&bin=te6ccgEBAgEAKQABIWQrfQcAAAAAAAAAABAX14QCAQAlF41FGQAAAAAAAAAAQ7msoAAQFA%3D%3D) # Installation and Initialization Source: https://tonutils.ness.uz/guide/installation-and-initialization Install tonutils and choose the right client — HTTP API providers, native ADNL lite clients, or balancers. This page explains how to install the `tonutils` library and select the appropriate client depending on your needs. ## Installation ```bash theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} pip install tonutils ``` Requires Python 3.9 or later. Native ADNL lite clients are included — no optional dependencies needed. ## Available Clients All clients share a common interface and are asynchronous context managers. Use `async with client:` or call `await client.connect()` / `await client.close()` explicitly. The network is selected with `NetworkGlobalID.MAINNET` or `NetworkGlobalID.TESTNET` from `ton_core`. Common constructor parameters: | Parameter | Description | | - | - | | `network` | Target TON network: `NetworkGlobalID.MAINNET` or `NetworkGlobalID.TESTNET`. | | `rps_limit` | Requests-per-period cap; match your API key tier. `None` means no client-side limit, except on keyless HTTP clients, which fall back to conservative defaults. | | `rps_period` | Rate-limit window in seconds (default: 1 second). | | `timeout` | Total request timeout in seconds (HTTP clients). | | `retry_policy` | Optional `RetryPolicy` with per-error rules. `None` disables retries. | Lite clients additionally accept: | Parameter | Description | | - | - | | `connect_timeout` | Timeout in seconds for a single connect/handshake attempt. | | `request_timeout` | Timeout in seconds for a single request (for balancers: total time including all failover attempts). | Ready-made retry policies are available in `tonutils.types`: `DEFAULT_HTTP_RETRY_POLICY` for HTTP clients and `DEFAULT_ADNL_RETRY_POLICY` for lite clients. Both are recommended for stability. ## HTTP Clients ### Toncenter **[toncenter.com](https://toncenter.com)** — official TON Center API (v2). API key is optional — without a key the client is limited to 1 request per 1.3 seconds. Obtain a key via [@toncenter](https://t.me/toncenter) for higher limits. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID from tonutils.clients import ToncenterClient client = ToncenterClient( network=NetworkGlobalID.MAINNET, api_key="", # optional rps_limit=1, ) async with client: ... ``` ### Tonapi **[tonapi.io](https://tonapi.io)** — Tonapi by Tonkeeper. API key is optional — without a key the client falls back to a conservative default rate limit (1 request per 4 seconds). Get a key at [tonconsole.com](https://tonconsole.com) for higher limits. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID from tonutils.clients import TonapiClient client = TonapiClient( network=NetworkGlobalID.MAINNET, api_key="", rps_limit=10, ) async with client: ... ``` ### Chainstack **[chainstack.com](https://chainstack.com)** — enterprise blockchain infrastructure. Requires a personal endpoint URL, obtained on the Chainstack website. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID from tonutils.clients import ChainstackClient client = ChainstackClient( network=NetworkGlobalID.MAINNET, http_provider_url="https://your-endpoint", rps_limit=50, ) async with client: ... ``` ### QuickNode **[quicknode.com](https://www.quicknode.com)** — high-performance RPC. Requires a personal endpoint URL, obtained on the QuickNode website. QuickNode supports mainnet only — there is no `network` parameter. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from tonutils.clients import QuicknodeClient client = QuicknodeClient( http_provider_url="https://your-endpoint", rps_limit=50, ) async with client: ... ``` ### Tatum **[tatum.io](https://tatum.io)** — multi-chain API platform. Requires an API key from [tatum.io](https://tatum.io). ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID from tonutils.clients import TatumClient client = TatumClient( network=NetworkGlobalID.MAINNET, api_key="", rps_limit=20, ) async with client: ... ``` ## HTTP Balancer `HttpBalancer` distributes requests across multiple HTTP clients. It selects the best available client based on limiter readiness and error rates, and falls back to round-robin when all clients are equally available. The `network` parameter is applied to every wrapped client, overriding their own setting (`QuicknodeClient` is rejected on testnet); `request_timeout` caps the total time of one operation including all failover attempts. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID from tonutils.clients import HttpBalancer, TonapiClient, ToncenterClient from tonutils.types import DEFAULT_HTTP_RETRY_POLICY toncenter = ToncenterClient( network=NetworkGlobalID.MAINNET, api_key="", rps_limit=1, retry_policy=DEFAULT_HTTP_RETRY_POLICY, ) tonapi = TonapiClient( network=NetworkGlobalID.MAINNET, api_key="", rps_limit=10, retry_policy=DEFAULT_HTTP_RETRY_POLICY, ) balancer = HttpBalancer( network=NetworkGlobalID.MAINNET, clients=[toncenter, tonapi], request_timeout=12.0, ) async with balancer: ... ``` ## Lite Clients `LiteClient` talks to a single lite server directly over ADNL TCP. There are three ways to create one. Avoid `rps_limit=1` with lite clients — parallel background queries are used to track masterchain updates. Private lite-server configs (available from [Tonconsole](https://tonconsole.com) or the [dTON bot](https://t.me/dtontech_bot)) are recommended for production; public servers may be unstable under load. Use when you have specific server credentials from a private provider. `ip` accepts a signed 32-bit integer or an IPv4 string; `public_key` accepts hex, base64, or bytes. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID from tonutils.clients import LiteClient from tonutils.types import DEFAULT_ADNL_RETRY_POLICY client = LiteClient( network=NetworkGlobalID.MAINNET, ip=-1234567890, port=12345, public_key="ABCdef0123=...", rps_limit=100, retry_policy=DEFAULT_ADNL_RETRY_POLICY, ) async with client: ... ``` Initialize from a private global config (recommended for production). `config` accepts a dict, a file path, or a `GlobalConfig`; `index` selects the lite server within the config's `liteservers` array. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID from tonutils.clients import LiteClient from tonutils.types import DEFAULT_ADNL_RETRY_POLICY client = LiteClient.from_config( network=NetworkGlobalID.MAINNET, config={}, # your provider's config index=0, rps_limit=50, retry_policy=DEFAULT_ADNL_RETRY_POLICY, ) async with client: ... ``` Fetches the lite-server list automatically from the public TON global config. Free, no credentials needed. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID from tonutils.clients import LiteClient from tonutils.types import DEFAULT_ADNL_RETRY_POLICY client = LiteClient.from_network_config( network=NetworkGlobalID.MAINNET, index=0, rps_limit=50, retry_policy=DEFAULT_ADNL_RETRY_POLICY, ) async with client: ... ``` ## Lite Balancer `LiteBalancer` manages multiple lite clients with automatic failover. It selects the best client based on masterchain height and ping RTT, falling back to round-robin when all clients are equally available. Compose the balancer from `LiteClient` instances created with private server credentials. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID from tonutils.clients import LiteBalancer, LiteClient from tonutils.types import DEFAULT_ADNL_RETRY_POLICY client_a = LiteClient( network=NetworkGlobalID.MAINNET, ip=-1234567890, port=12345, public_key="Abc123...", rps_limit=50, retry_policy=DEFAULT_ADNL_RETRY_POLICY, ) client_b = LiteClient( network=NetworkGlobalID.MAINNET, ip=-987654321, port=54321, public_key="Zyx987...", rps_limit=50, retry_policy=DEFAULT_ADNL_RETRY_POLICY, ) balancer = LiteBalancer( network=NetworkGlobalID.MAINNET, clients=[client_a, client_b], connect_timeout=2, request_timeout=12, ) async with balancer: ... ``` Automatically creates a `LiteClient` for each server in a private config. `rps_per_client=False` (default) shares one rate limiter across all clients; `True` gives each client its own limiter. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID from tonutils.clients import LiteBalancer from tonutils.types import DEFAULT_ADNL_RETRY_POLICY balancer = LiteBalancer.from_config( network=NetworkGlobalID.MAINNET, config={}, # your provider's config rps_limit=100, retry_policy=DEFAULT_ADNL_RETRY_POLICY, ) async with balancer: ... ``` Uses all lite servers from the public TON global config. Free, no credentials needed. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID from tonutils.clients import LiteBalancer from tonutils.types import DEFAULT_ADNL_RETRY_POLICY balancer = LiteBalancer.from_network_config( network=NetworkGlobalID.MAINNET, rps_limit=50, connect_timeout=1, request_timeout=10, retry_policy=DEFAULT_ADNL_RETRY_POLICY, ) async with balancer: ... ``` Full runnable initialization scripts for every client and balancer on GitHub. # Usage Scenarios Source: https://tonutils.ness.uz/guide/usage-scenarios Practical examples — creating and importing wallets, sending transactions, reading contract data, and calling get-methods. This section is a condensed tour of the `tonutils` library: creating and importing wallets, sending transactions, retrieving contract data, and calling get-methods. The snippets here are fragments for orientation — each topic has a full runnable version under [Usage Examples](/guide/examples/wallet-operations). ## Working with Wallets ### Creating a Wallet ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID, WalletV4Config from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 client = ToncenterClient(network=NetworkGlobalID.TESTNET) # Wallet configuration (default settings) # Customize subwallet_id to derive multiple wallets from the same mnemonic config = WalletV4Config() wallet, public_key, private_key, mnemonic = WalletV4R2.create(client, config=config) print(f"Address: {wallet.address.to_str(is_bounceable=False)}") print(f"Mnemonic: {' '.join(mnemonic)}") ``` **Result:** * `wallet` — an instance of `WalletV4R2`, ready to use. * `public_key` — the wallet's public key (`PublicKey`). * `private_key` — the wallet's private key (`PrivateKey`). * `mnemonic` — list of 24 words (`list[str]`). ### Importing a Wallet From a mnemonic phrase: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} wallet, public_key, private_key, mnemonic = WalletV4R2.from_mnemonic(client, MNEMONIC) ``` **Parameters:** * `MNEMONIC` — can be provided in one of two formats: * a string of space-separated words. * a list of words. Or from an existing private key: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import PrivateKey wallet = WalletV4R2.from_private_key(client, PrivateKey("")) ``` **Parameters:** * `private_key` — a `PrivateKey`, constructed from raw bytes, a hex string, a base64 string, or an integer. Full runnable scripts for creating, importing, and inspecting wallets on GitHub. ## Sending Transactions ### Single Transaction ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, to_nano msg = await wallet.transfer( destination=Address("UQ..."), amount=to_nano(0.01), body="Hello from tonutils!", ) print(f"Transaction hash: {msg.normalized_hash}") ``` **Parameters:** * `destination` — the recipient: a string with the address or an `Address` object. * `amount` — amount in nanotons (`int`); use `to_nano()` to convert from TON (1 TON = 10^9 nanotons). * `body` — comment as a string or custom data as a `Cell`. **Result:** An `ExternalMessage`. Its `normalized_hash` is the normalized hash of the signed external message, computed locally before sending — it is not the on-chain transaction hash. Use it to track whether the message was accepted on-chain (via explorers or API queries). ### Batch Transfer Allows sending multiple transfers in a single transaction: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, to_nano from tonutils.contracts import TONTransferBuilder msg = await wallet.batch_transfer_message( [ TONTransferBuilder( destination=Address("UQ..."), amount=to_nano(0.01), body="Hello from tonutils!", ), TONTransferBuilder( destination=Address("UQ..."), amount=to_nano(0.02), body="Hello from tonutils!", ), ] ) print(f"Transaction hash: {msg.normalized_hash}") ``` **Result:** An `ExternalMessage` (see [Single Transaction](#single-transaction) for the meaning of `normalized_hash`). V3 and V4 wallets support up to 4 messages per transaction. For more messages, use `WalletV5R1`, `WalletHighloadV3R1`, or `WalletPreprocessedV2`. ### Send Mode The `send_mode` parameter controls how the outgoing message is sent — for example, mode `128` carries the entire remaining balance of the wallet. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} await wallet.transfer( destination="UQ...", amount=0, send_mode=128, ) ``` For more details on available send modes, see the [official TON documentation](https://docs.ton.org/foundations/messages/modes). ## Contract Information ### Contract Balance ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import to_amount balance = (await client.get_info("UQ...")).balance print(f"Balance: {balance} ({to_amount(balance)} TON)") ``` **Result:** An integer (`int`) representing the contract balance in nanotons. Use `to_amount()` to convert nanotons to TON. ### Contract Data ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} info = await client.get_info("UQ...") ``` **Result:** A `ContractInfo` object; see [ContractInfo Fields Overview](/how-to/get-contract-information#contractinfo-fields-overview) for the full field reference. ## Calling a Contract Method The `run_get_method` function allows invoking a contract's get-method. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} result = await client.run_get_method( address="UQ...", method_name="get_my_data", stack=[0], ) ``` **Parameters:** * `address` — contract address as a string or an `Address` object. * `method_name` — name of the get-method to call (`str`). * `stack` — list of arguments, or `None` for no arguments. **Result:** A list of values returned from the method's execution stack. # Create Multiple Wallets Source: https://tonutils.ness.uz/how-to/create-multiple-wallets Derive multiple wallet addresses from a single mnemonic using subwallet_id. This example shows how to generate **multiple wallet addresses** from a single mnemonic by changing the `subwallet_id` in the wallet config. Each `subwallet_id` produces a unique address under the same seed phrase. ## Example ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID, WalletV4Config from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Mnemonic phrase MNEMONIC = "word1 word2 word3 ..." # Subwallet ID SUBWALLET_ID = 698983191 def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) config = WalletV4Config(subwallet_id=SUBWALLET_ID) wallet, public_key, private_key, mnemonic = WalletV4R2.from_mnemonic( client, MNEMONIC, config=config ) print(f"Address: {wallet.address.to_str(is_bounceable=False)}") if __name__ == "__main__": main() ``` ## Explanation * `subwallet_id` — a 32-bit integer used to derive distinct wallet addresses from the same mnemonic. It is part of the wallet config (`WalletV3Config`, `WalletV4Config`, etc. from `ton_core`), passed to `.from_mnemonic()` / `.from_private_key()` / `.create()` via the `config=` argument. * **Use cases**: * Splitting funds across logical accounts. * Managing separate balances. * Creating derived wallets for contract operations. ## Important Notes * `subwallet_id` **is not supported** in: * `WalletV1*` * `WalletV2*` * `WalletPreprocessedV2` * **Default value**: for all wallet types with an integer `subwallet_id` (`WalletV3*`, `WalletV4*`, `WalletHighloadV2`, `WalletHighloadV3R1`) the default is `698983191` (`DEFAULT_SUBWALLET_ID` from `ton_core`). * `WalletV5R1` and `WalletV5Beta` use a structured identifier instead of a plain integer: `WalletV5SubwalletID` from `ton_core`, with fields `subwallet_number` (default `0`), `workchain` (`WorkchainID`, default basechain), `version` (default `0`), and `network` (`NetworkGlobalID`, default mainnet). If `subwallet_id` is left unset in the config, the library fills it in using the client's network. `WalletV5R1` packs the fields into a 32-bit integer XORed with the network global ID; `WalletV5Beta` stores the four fields separately in the wallet data: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID, WalletV5Config, WalletV5SubwalletID from tonutils.contracts import WalletV5R1 config = WalletV5Config( subwallet_id=WalletV5SubwalletID( subwallet_number=1, network=NetworkGlobalID.MAINNET, ) ) wallet, *_ = WalletV5R1.from_mnemonic(client, MNEMONIC, config=config) ``` Default packed values (`WalletV5R1`): | network | workchain | version | subwallet\_number | packed wallet\_id | | - | - | - | - | - | | -239 | 0 | 0 | 0 | 2147483409 | | -239 | -1 | 0 | 0 | 8388369 | | -3 | 0 | 0 | 0 | 2147483645 | | -3 | -1 | 0 | 0 | 8388605 | # Get Contract Code and Data Source: https://tonutils.ness.uz/how-to/get-contract-code-and-data Retrieve contract code and data using TON explorers or the get_info method. To retrieve the code and data of a contract, use TON explorers such as [tonviewer.com](https://tonviewer.com), [tonscan.org](https://tonscan.org), and others, or use the `get_info` method programmatically. ## Using TON Explorers 1. Open [Tonviewer](https://tonviewer.com). 2. Enter the contract address into the search field. 3. Navigate to the **Code** tab. 4. The **Bytecode** section contains the contract code. 5. The **Raw data** section contains the contract data. ## Using the get\_info Method ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID from tonutils.clients import ToncenterClient # Contract address to inspect CONTRACT_ADDRESS = "EQ..." async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() info = await client.get_info(CONTRACT_ADDRESS) # Print contract code (hex-encoded BoC) print(info.code.to_boc().hex()) # Print contract data (hex-encoded BoC) print(info.data.to_boc().hex()) await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` `info.code` and `info.data` are `None` for contracts that are not deployed. The raw hex-encoded BoC strings are also available directly as `info.code_raw` and `info.data_raw`. See also [ContractInfo Fields Overview](/how-to/get-contract-information#contractinfo-fields-overview). # Get Contract Information Source: https://tonutils.ness.uz/how-to/get-contract-information Retrieve contract balance, state, code, data, and last transaction metadata. This example shows how to retrieve full contract details, including balance, state, code, data, and last transaction metadata, using the `get_info` method available on every client. ## Example ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID from tonutils.clients import ToncenterClient # Contract address to inspect CONTRACT_ADDRESS = "EQ..." async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() info = await client.get_info(CONTRACT_ADDRESS) print(f"Balance: {info.balance}") print(f"State: {info.state}") print(f"Has code: {info.code is not None}") print(f"Has data: {info.data is not None}") print(f"Last Transaction LT: {info.last_transaction_lt}") print(f"Last Transaction Hash: {info.last_transaction_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ## ContractInfo Fields Overview | Field | Type | Description | | - | - | - | | `code_raw` | `str \| None` | Hex-encoded BoC of the contract code, or `None` if absent. | | `data_raw` | `str \| None` | Hex-encoded BoC of the contract data, or `None` if absent. | | `balance` | `int` | Current balance of the contract in nanotons. | | `state` | `ContractState` | Contract lifecycle state (enum from `ton_core`): `ACTIVE` (deployed and operational), `FROZEN` (frozen due to storage-fee debt), `UNINIT` (address exists but code is not deployed), `NONEXIST` (no balance or state). | | `code` | `Cell \| None` | Contract code parsed from `code_raw`, or `None` if absent (property). | | `data` | `Cell \| None` | Contract data parsed from `data_raw`, or `None` if absent (property). | | `last_transaction_lt` | `int \| None` | Logical time (LT) of the most recent transaction, or `None`. | | `last_transaction_hash` | `str \| None` | Hash of the most recent transaction, or `None`. | | `state_init` | `StateInit` | `StateInit` combining `code` and `data` (property). | # Get Jetton Wallet Address Source: https://tonutils.ness.uz/how-to/get-jetton-wallet-address Obtain a jetton wallet address via the master's get-method or by calculating it locally. There are two ways to obtain the address of a jetton wallet: call the `get_wallet_address` get-method on the jetton master, or calculate the address locally from the wallet code. ## Using Get-Method Works for any jetton master (Standard and Stablecoin) — the contract itself resolves the wallet address. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, NetworkGlobalID from tonutils.clients import ToncenterClient from tonutils.contracts import get_wallet_address_get_method # Wallet owner's address OWNER_ADDRESS = Address("UQ...") # Deployed jetton master contract address JETTON_MASTER_ADDRESS = Address("EQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() jetton_wallet_address = await get_wallet_address_get_method( client=client, address=JETTON_MASTER_ADDRESS, owner_address=OWNER_ADDRESS, ) print(jetton_wallet_address.to_str()) await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ## Calculating Locally No network calls — the address is derived from the owner's address, the master address, and the jetton wallet code. Prepare the jetton wallet contract code by following [Get Contract Code and Data](/how-to/get-contract-code-and-data). `jetton_wallet_code` accepts a `Cell` or a hex-encoded BoC string. If the jetton uses the default code, `JettonWalletStandard.get_default_code()` / `JettonWalletStablecoinV2.get_default_code()` return it directly. ### Standard Jetton ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from tonutils.contracts import JettonMasterStandard def main() -> None: owner_address = "UQ..." jetton_master_address = "EQ..." jetton_wallet_code = "..." jetton_wallet_address = JettonMasterStandard.calculate_user_jetton_wallet_address( owner_address=owner_address, jetton_master_address=jetton_master_address, jetton_wallet_code=jetton_wallet_code, ) print(jetton_wallet_address.to_str()) if __name__ == "__main__": main() ``` ### Stablecoin Jetton Use `JettonMasterStablecoinV2` for sharded V2 contracts, or `JettonMasterStablecoin` for V1 contracts (e.g. USD₮, NOT). ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from tonutils.contracts import JettonMasterStablecoinV2 def main() -> None: owner_address = "UQ..." jetton_master_address = "EQ..." jetton_wallet_code = "..." jetton_wallet_address = JettonMasterStablecoinV2.calculate_user_jetton_wallet_address( owner_address=owner_address, jetton_master_address=jetton_master_address, jetton_wallet_code=jetton_wallet_code, ) print(jetton_wallet_address.to_str()) if __name__ == "__main__": main() ``` # Get NFT Item Address Source: https://tonutils.ness.uz/how-to/get-nft-item-address Resolve an NFT item address by index via the collection get-method or calculate it locally, including TON DNS domains and Telegram collections. There are several ways to obtain the address of an NFT item: call the collection's `get_nft_address_by_index` get-method on-chain, or calculate the address locally from the item index, item code, and collection address. ## Standard Collections ### Using Get-Method Use `get_nft_address_by_index_get_method()` to call `get_nft_address_by_index` on the collection contract. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, NetworkGlobalID from tonutils.clients import ToncenterClient from tonutils.contracts import get_nft_address_by_index_get_method # NFT item index within the collection NFT_ITEM_INDEX = 1 # Deployed NFT collection contract address NFT_COLLECTION_ADDRESS = Address("EQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() nft_item_address = await get_nft_address_by_index_get_method( client=client, address=NFT_COLLECTION_ADDRESS, index=NFT_ITEM_INDEX, ) print(f"NFT item address: {nft_item_address.to_str()}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Calculating Locally Use `NFTCollectionStandard.calculate_nft_item_address()` to derive the address without any network calls. The address is the hash of the item's initial state, built from the index, the item contract code, and the collection address. Obtain the NFT item contract code by following [Get Contract Code and Data](/how-to/get-contract-code-and-data). `nft_item_code` accepts a `Cell` or a hex-encoded BoC string. For collections deployed with the default tonutils item code, `NFTItemStandard.get_default_code()` returns it directly. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address from tonutils.contracts import NFTCollectionStandard # NFT item index within the collection NFT_ITEM_INDEX = 1 # NFT item contract code (hex-encoded BoC string or Cell) NFT_ITEM_CODE = "..." # Deployed NFT collection contract address NFT_COLLECTION_ADDRESS = Address("EQ...") def main() -> None: 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()}") if __name__ == "__main__": main() ``` ## TON DNS Domains The item index of a TON DNS domain is `string_hash(domain_name)` — the SHA-256 hash of the domain name without the zone suffix (e.g. `"temp"` for `temp.ton`). See [Use FunC Hash Functions](/how-to/use-func-hash-functions). ### Using Get-Method Compute the domain index with `string_hash()` and pass it to the same get-method helper. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, NetworkGlobalID, string_hash from tonutils.clients import ToncenterClient from tonutils.contracts import get_nft_address_by_index_get_method # Domain name without the zone suffix (e.g. "temp" for temp.ton) DOMAIN_NAME = "temp" # TON DNS collection contract address DNS_COLLECTION_ADDRESS = Address("EQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() domain_index = string_hash(DOMAIN_NAME) nft_item_address = await get_nft_address_by_index_get_method( client=client, address=DNS_COLLECTION_ADDRESS, index=domain_index, ) print(f"NFT item address: {nft_item_address.to_str()}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Calculating Locally `TONDNSCollection.calculate_nft_item_address()` accepts either the domain name string (the index is derived automatically) or a precomputed numeric index. Obtain the DNS item contract code by following [Get Contract Code and Data](/how-to/get-contract-code-and-data). ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address from tonutils.contracts import TONDNSCollection # Domain name without the zone suffix (e.g. "temp" for temp.ton) DOMAIN_NAME = "temp" # DNS item contract code (hex-encoded BoC string or Cell) NFT_ITEM_CODE = "..." # TON DNS collection contract address DNS_COLLECTION_ADDRESS = Address("EQ...") def main() -> None: nft_item_address = TONDNSCollection.calculate_nft_item_address( index_or_domain=DOMAIN_NAME, nft_item_code=NFT_ITEM_CODE, collection_address=DNS_COLLECTION_ADDRESS, ) print(f"NFT item address: {nft_item_address.to_str()}") if __name__ == "__main__": main() ``` ## Telegram Gifts and Anonymous Numbers The item index of a Telemint token is `string_hash(telemint_token_name)` — the SHA-256 hash of the token name (e.g. the number `"8888888"` or a username). See [Use FunC Hash Functions](/how-to/use-func-hash-functions). ### Using Get-Method ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, NetworkGlobalID, string_hash from tonutils.clients import ToncenterClient from tonutils.contracts import get_nft_address_by_index_get_method # Telemint token name (e.g. anonymous number or gift slug) TELEMINT_TOKEN_NAME = "8888888" # Telemint collection contract address NFT_COLLECTION_ADDRESS = Address("EQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() token_index = string_hash(TELEMINT_TOKEN_NAME) nft_item_address = await get_nft_address_by_index_get_method( client=client, address=NFT_COLLECTION_ADDRESS, index=token_index, ) print(f"NFT item address: {nft_item_address.to_str()}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ### Calculating Locally Telemint items store their initial data in a different layout than standard collections, so use the Telegram collection classes: `TelegramGiftsCollection` for gifts and anonymous numbers, `TelegramUsernamesCollection` for `t.me` usernames. Both share the same `calculate_nft_item_address()` implementation. Obtain the Telemint item contract code by following [Get Contract Code and Data](/how-to/get-contract-code-and-data). ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, string_hash from tonutils.contracts import TelegramGiftsCollection # Telemint token name (e.g. anonymous number or gift slug) TELEMINT_TOKEN_NAME = "8888888" # Telemint item contract code (hex-encoded BoC string or Cell) NFT_ITEM_CODE = "..." # Telemint collection contract address NFT_COLLECTION_ADDRESS = Address("EQ...") def main() -> None: token_index = string_hash(TELEMINT_TOKEN_NAME) nft_item_address = TelegramGiftsCollection.calculate_nft_item_address( index=token_index, nft_item_code=NFT_ITEM_CODE, collection_address=NFT_COLLECTION_ADDRESS, ) print(f"NFT item address: {nft_item_address.to_str()}") if __name__ == "__main__": main() ``` # Send Encrypted Message Source: https://tonutils.ness.uz/how-to/send-encrypted-message Send a TON transfer with an end-to-end encrypted comment and decrypt it on the receiving side. This example shows how to send a **TON transfer with an encrypted comment**. The comment is encrypted end-to-end (ECDH) so that only the sender and the intended recipient can read it. ## Encrypt and Send Retrieve the recipient's public key from their deployed wallet contract with `get_public_key_get_method`, encrypt the comment with `TextCipher.encrypt`, and send it as the transfer body. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} 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 phrase MNEMONIC = "word1 word2 word3 ..." # Destination address (recipient) # Recipient must have a deployed wallet contract with a public key # accessible via the get_public_key get-method DESTINATION_ADDRESS = Address("UQ...") async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) await client.connect() # Keep private_key for encryption as our_private_key wallet, _, our_private_key, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) # Retrieve recipient's public key from the blockchain # Raises an error if the wallet is uninit or lacks the get_public_key method their_public_key = await get_public_key_get_method( client=client, address=DESTINATION_ADDRESS, ) # Encrypt the message; only the sender and the recipient can decrypt it body = TextCipher.encrypt( payload="Hello from tonutils!", sender_address=wallet.address, our_private_key=our_private_key, their_public_key=their_public_key, ) msg = await wallet.transfer( destination=DESTINATION_ADDRESS, amount=to_nano(0.01), body=body, ) # Normalized hash of the signed external message (computed locally) — # not the on-chain transaction hash print(f"Transaction hash: {msg.normalized_hash}") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ## Decrypt Use `TextCipher.decrypt` with the recipient's private key and the original sender's address. The encrypted payload (message body with opcode `0x2167DA4B`) can be provided as raw `bytes`, a hex string, a base64 string, or a full body `Cell` parsed from BoC. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, NetworkGlobalID, TextCipher from tonutils.clients import ToncenterClient from tonutils.contracts import WalletV4R2 # Recipient's mnemonic phrase MNEMONIC = "word1 word2 word3 ..." # Original sender's address (used for verification) SENDER_ADDRESS = Address("UQ...") # Encrypted payload extracted from the transaction body ENCRYPTED_PAYLOAD = "" async def main() -> None: client = ToncenterClient(network=NetworkGlobalID.MAINNET) # Keep private_key for decryption as our_private_key _, _, our_private_key, _ = WalletV4R2.from_mnemonic(client, MNEMONIC) decrypted_comment = TextCipher.decrypt( payload=ENCRYPTED_PAYLOAD, sender_address=SENDER_ADDRESS, our_private_key=our_private_key, ) print(f"Decrypted message: {decrypted_comment}") if __name__ == "__main__": import asyncio asyncio.run(main()) ``` The same example also appears in [Encrypted Messages](/guide/examples/wallet-operations#encrypted-messages) among the other wallet operations. # Send Gasless Transaction Source: https://tonutils.ness.uz/how-to/send-gasless-transaction Send a gasless jetton transfer via the Tonapi relay, paying fees in jettons instead of TON. This example demonstrates how to send a **gasless** jetton transfer. A relay pays the TON gas on behalf of the sender, and the commission is deducted from the sender's jetton balance. Gasless transfers use the relayer feature provided by [tonapi.io](https://tonapi.io) and require a `TonapiClient` with an API key from [tonconsole.com](https://tonconsole.com). They are only supported on **mainnet** and only for **WalletV5** (`WalletV5R1` and `WalletV5Beta`). ## Example ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, NetworkGlobalID, to_nano from tonutils.clients import TonapiClient from tonutils.contracts import WalletV5R1 # Tonapi API key (required for gasless transfers) # Get one at https://tonconsole.com/ API_KEY = "YOUR_API_KEY" # Mnemonic phrase MNEMONIC = "word1 word2 word3 ..." # Jetton master contract address # This jetton is used both for the transfer and for gas payment # Supported jettons: check https://tonapi.io/v2/gasless/config JETTON_MASTER_ADDRESS = Address("EQ...") # Destination address (jetton recipient) DESTINATION_ADDRESS = Address("UQ...") # Jetton amount in base units (respects token decimals) # Example: 1 USDT (6 decimals) -> 1 * 10^6 = 1,000,000 JETTON_AMOUNT_TO_SEND = to_nano(1, decimals=6) async def main() -> None: client = TonapiClient(network=NetworkGlobalID.MAINNET, api_key=API_KEY) await client.connect() wallet, _, _, _ = WalletV5R1.from_mnemonic(client, MNEMONIC) # Step 1: Estimate the gasless transfer # Builds a jetton transfer message, validates provider and jetton support, # and sends it to the Tonapi gasless estimation endpoint. # Parameters mirror JettonTransferBuilder. estimate = await wallet.gasless_estimate( destination=DESTINATION_ADDRESS, jetton_amount=JETTON_AMOUNT_TO_SEND, jetton_master_address=JETTON_MASTER_ADDRESS, ) # Step 2: Review the estimation result before sending # estimate.commission — relay fee (deducted from the jetton balance) # estimate.relay_address — address of the relay paying gas # estimate.valid_until — unix timestamp, the transaction expires after this time # estimate.messages — messages to sign (built by the relay) # estimate.emulation — transaction emulation with trace and risk analysis print(f"Commission: {estimate.commission} nanocoins") print(f"Relay: {estimate.relay_address}") print(f"Valid until: {estimate.valid_until}") # Step 3: Sign and send via the gasless relay await wallet.gasless_send(estimate) print("Gasless transfer sent!") await client.close() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` Always verify the estimated commission and emulation result before calling `gasless_send` — once sent, the transaction is irreversible. The same example also appears in [Gasless Transfers](/guide/examples/wallet-operations#gasless-transfers) among the other wallet operations. # Use FunC Hash Functions Source: https://tonutils.ness.uz/how-to/use-func-hash-functions Compute cell, slice, and string hashes matching the FunC stdlib functions. The `ton_core` package provides Python equivalents of the FunC stdlib hash functions `cell_hash`, `slice_hash`, and `string_hash`. Use them to reproduce on-chain hash computations off-chain, for example when signing or verifying data structured as cells. ## cell\_hash Calculates the representation hash of the given cell `c` and returns it as a 256-bit unsigned integer `x`. This function is handy for signing and verifying signatures of arbitrary entities structured as a tree of cells. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import begin_cell, cell_hash def main() -> None: c = begin_cell().store_string("ness").end_cell() x = cell_hash(c) print(x) if __name__ == "__main__": main() ``` ## slice\_hash Computes the hash of the given slice `s` and returns it as a 256-bit unsigned integer `x`. The result is equivalent to creating a standard cell containing only the data and references from `s` and then computing its hash using `cell_hash`. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import begin_cell, slice_hash def main() -> None: s = begin_cell().store_string("ness").to_slice() x = slice_hash(s) print(x) if __name__ == "__main__": main() ``` ## string\_hash Calculates the SHA-256 hash of the UTF-8 bytes of the given string `s` and returns it as a 256-bit unsigned integer `x`. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import string_hash def main() -> None: s = "ness" x = string_hash(s) print(x) if __name__ == "__main__": main() ``` These helpers are used by [Get NFT Item Address](/how-to/get-nft-item-address) to derive item indexes for TON DNS domains and Telemint tokens. # Overview Source: https://tonutils.ness.uz/index Python SDK for the TON blockchain — wallets, contracts, and more via ADNL and HTTP APIs. **Tonutils** is a high-level, object-oriented Python SDK for the TON blockchain. It provides wallets, transfers, smart contracts, and on-chain tools over both native lite servers (ADNL) and popular HTTP API providers. Looking for versions below 2.0? The [legacy documentation](https://old-tonutils.ness.uz) is still available. For a better understanding of TON internal architecture and core principles, review the [official TON documentation](https://docs.ton.org). ## Features * **Providers** — HTTP APIs ([Toncenter](https://toncenter.com), [Tonapi](https://tonapi.io), [Chainstack](https://chainstack.com), [QuickNode](https://www.quicknode.com), [Tatum](https://tatum.io)) and native ADNL lite clients, plus balancers with automatic failover for both. * **Wallets** — create, import, and deploy all standard wallet versions (V1–V5, Highload, Preprocessed). * **Transfers** — TON, jettons, and NFTs, including batch, gasless, and encrypted transfers. * **Contracts** — deploy, mint, and manage jetton, NFT, and DNS contracts. * **Tools** — block scanner, lite server and DHT status monitors, and a CLI. TON Connect is no longer part of tonutils. It lives in the separate [`ton-connect`](https://pypi.org/project/ton-connect/) package, documented in the [TON Connect](/ton-connect) section of this site. ## Installation ```bash theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} pip install tonutils ``` | Requirement | Version | | - | - | | Python | 3.9+ | ## Explore Install the library and initialize HTTP or lite clients. Wallets, transfers, jettons, NFTs, and DNS operations. Focused recipes for common on-chain tasks. Block scanner, status monitors, and the CLI. Connect wallets, send transactions, and verify proofs with the `ton-connect` package. Full runnable scripts: wallets, transfers, jettons, NFTs, and more. ## Contribution Contributions are welcome. If you have ideas for improvements or have identified a bug, create an issue or submit a pull request on [GitHub](https://github.com/nessshon/tonutils/pulls). # Backend Verification Source: https://tonutils.ness.uz/ton-connect/backend-verification Verify TON Proof and signed data payloads on your Python backend, independently of how the wallet was connected. The verification utilities work standalone: a frontend (for example, a JS app using TonConnect UI) connects the wallet and sends the resulting proof or signature to your Python backend, which validates it cryptographically. Never trust a client-reported address — only a verified TON Proof or signed-data payload proves that the user actually controls the private key of the declared address. ## Verifying TON Proof TON Proof is connection-time authentication: the wallet signs a message bound to its address, your app's domain, a timestamp, and a backend-issued payload nonce. The frontend sends your backend a dict in this shape: ```json theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} { "address": "0:83ae019a23a8162beaa5cb0ebdc56668b2eac6c6ba51808812915b206a152dc5", "network": "-239", "publicKey": "79c446597dbf81b9987e9059de95dc557bcd9e2c431a6db1677768783d0b99f7", "walletStateInit": "te6cck...", "proof": { "timestamp": 1754535788, "domain": { "lengthBytes": 10, "value": "github.com" }, "signature": "d8GGkz...CQ==", "payload": "f85774c9762007d20000000068941ae3" } } ``` | Field | Description | | - | - | | `address` | Raw wallet address (`workchain:hash` format). | | `network` | `"-239"` for mainnet, `"-3"` for testnet. | | `publicKey` | Wallet's Ed25519 public key (hex). | | `walletStateInit` | Base64-encoded wallet `StateInit` — lets the backend derive and verify the public key even for wallets not yet deployed on-chain. | | `proof.timestamp` | Unix timestamp (seconds) when the wallet signed — checked against `valid_auth_time`. | | `proof.domain` | App domain the wallet signed for — must be in `allowed_domains`. | | `proof.signature` | Ed25519 signature over the assembled `ton-proof-item-v2` message (base64). | | `proof.payload` | Nonce issued by your backend — ties the proof to your specific auth request. | Validate the dict into a typed DTO with `TonProofPayloadDto.model_validate(...)` and verify it with `VerifyTonProof`. Verification performs five checks and raises `nacl.exceptions.BadSignatureError` if any fails: 1. The public key extracted from `walletStateInit` matches the `publicKey` field. 2. The address derived from `walletStateInit` matches the `address` field. 3. The timestamp is within the `valid_auth_time` window (prevents replay). 4. The domain is in `allowed_domains` (prevents cross-app reuse). 5. The Ed25519 signature over the `ton-proof-item-v2` message is valid. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_connect import VerifyTonProof from ton_connect.models import TonProofPayloadDto # Payload received from the frontend after wallet connection via TonConnect PAYLOAD = { "address": "0:83ae019a23a8162beaa5cb0ebdc56668b2eac6c6ba51808812915b206a152dc5", "network": "-239", "publicKey": "79c446597dbf81b9987e9059de95dc557bcd9e2c431a6db1677768783d0b99f7", "walletStateInit": "te6cck...", "proof": { "timestamp": 1754535788, "domain": { "lengthBytes": 10, "value": "github.com", }, "signature": "d8GGkz...CQ==", "payload": "f85774c9762007d20000000068941ae3", }, } async def main() -> None: # Parse and validate the raw payload dict into a typed DTO payload = TonProofPayloadDto.model_validate(PAYLOAD) # Raises BadSignatureError if any check fails # allowed_domains: list of domains your backend accepts — reject anything else # valid_auth_time: max age of the proof in seconds (15 min is typical) await VerifyTonProof(payload).verify( allowed_domains=["github.com"], valid_auth_time=15 * 60, # 15 minutes ) print("TonProof verified") if __name__ == "__main__": import asyncio asyncio.run(main()) ``` The public key can only be extracted locally from `walletStateInit` for known standard wallet contracts (V1-V5). For wallet contracts the library does not recognize, pass an async resolver as `get_wallet_public_key` to `verify(...)` — it receives the wallet `Address` and should return the `PublicKey` (for example, by calling the contract's `get_public_key` get method on-chain) or `None`. ## Verifying Signed Data Unlike TON Proof, `signData` is called explicitly to sign off-chain content — agreeing to terms, authorizing actions, and similar. The frontend sends a flat dict with the signature fields at the top level: ```json theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} { "address": "0:83ae019a23a8162beaa5cb0ebdc56668b2eac6c6ba51808812915b206a152dc5", "network": "-239", "publicKey": "79c446597dbf81b9987e9059de95dc557bcd9e2c431a6db1677768783d0b99f7", "walletStateInit": "te6cck...", "signature": "D802oc...DA==", "timestamp": 1754503448, "domain": "github.com", "payload": {"type": "text", "text": "Hello from ton-connect!"} } ``` `payload` is the data the user signed; supported types are `text`, `binary`, and `cell`. Verification follows the same pattern with `SignDataPayloadDto` and `VerifySignData`, performing the same five checks. The canonical signing message varies by payload type: text and binary use SHA-256, cell payloads use a TL-B cell hash with a CRC32 schema fingerprint. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_connect import VerifySignData from ton_connect.models import SignDataPayloadDto # Payload received from the frontend after the user signs data via TonConnect PAYLOAD = { "address": "0:83ae019a23a8162beaa5cb0ebdc56668b2eac6c6ba51808812915b206a152dc5", "network": "-239", "publicKey": "79c446597dbf81b9987e9059de95dc557bcd9e2c431a6db1677768783d0b99f7", "walletStateInit": "te6cck...", "signature": "D802oc...DA==", "timestamp": 1754503448, "domain": "github.com", "payload": {"type": "text", "text": "Hello from ton-connect!"}, } async def main() -> None: # Parse and validate the raw payload dict into a typed DTO payload = SignDataPayloadDto.model_validate(PAYLOAD) # Raises BadSignatureError if any check fails # valid_auth_time: max age of the signature in seconds (keep short, 5 min typical) await VerifySignData(payload).verify( allowed_domains=["github.com"], valid_auth_time=5 * 60, # 5 minutes ) print("SignData verified") if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ## Challenge Payloads A TON Proof is only meaningful when its `payload` nonce was issued by your backend. Otherwise an attacker could replay a valid proof the user produced for another service. The library provides an HMAC-based challenge that requires no server-side session state: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_connect import create_ton_proof_payload, verify_ton_proof_payload SECRET_KEY = "your-secret-key" # keep this on your backend # Before connecting: issue a challenge and pass it to the frontend, # which includes it in the connect request ton_proof_payload = create_ton_proof_payload(secret_key=SECRET_KEY, ttl=15 * 60) # After connecting: check the nonce that came back inside the proof # Raises BadSignatureError if the HMAC is invalid or the payload has expired verify_ton_proof_payload(secret_key=SECRET_KEY, ton_proof_payload=proof_payload_from_wallet) ``` The payload contains random bytes, an expiry timestamp, and an HMAC-SHA256 signature under your secret key. `verify_ton_proof_payload` confirms the nonce was issued by your backend and is still within its TTL, which binds the proof to your service and prevents replay. Run it before `VerifyTonProof(...).verify(...)`. For the full connect-time flow where the same Python process both connects the wallet and verifies the proof, see [Connect Wallet](/ton-connect/connect-wallet#connecting-with-ton-proof). Standalone TON Proof verification example on GitHub. Standalone signed-data verification example on GitHub. # Connect Wallet Source: https://tonutils.ness.uz/ton-connect/connect-wallet Restore sessions, generate connect links, authenticate users with TON Proof, and import or export TonConnect UI sessions. This page covers the wallet connection flow: restore, connect links, TON Proof, and TonConnect UI session import/export. All snippets assume the setup from [Installation and Initialization](/ton-connect/installation-and-initialization) — a `TonConnect` instance and a `connector` created with `tc.create_connector(SESSION_KEY)`. ## Restoring a Session `restore()` resumes a previously saved session from storage and returns `True` if an active one was found: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} restored = await connector.restore() if restored: print(f"Address: {connector.account.address.to_str(is_bounceable=False)}") ``` Always call `restore()` first — starting a new connection while one is already active raises `WalletAlreadyConnectedError`. ## Connecting a Wallet A connection starts with a `ConnectRequest` built by `make_connect_request()`, which is then passed to `connect()`. The call opens a bridge connection and returns the standard `tc://` universal link that any TonConnect-compatible wallet can open: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID request = connector.make_connect_request() url = await connector.connect( request=request, network=NetworkGlobalID.TESTNET, ) print(f"Connect URL: {url}") ``` The `network` parameter is a guard: if the wallet that approves the request is connected to a different chain, the connection fails with `WalletWrongNetworkError` (returned as the error from `wait_connect()`). Pass `None` to accept any network. The library returns plain URL strings. Rendering them as a QR code (or as a button in a Telegram bot) is up to your application — any QR library works on the returned string. ### Wallet-Specific Links `make_connect_url()` builds a link based on a specific wallet's universal URL, so it opens directly in that wallet app instead of showing a chooser: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} tonkeeper = app_wallets_loader.get_wallet("tonkeeper") tonkeeper_url = connector.make_connect_url(request, tonkeeper) print(f"Tonkeeper URL: {tonkeeper_url}") ``` Bridge connections are not affected by which link you display — the connector listens on the bridges of all loaded wallets, so you can show several wallet-specific links for the same request. ## Waiting for the Connection `wait_connect()` blocks until the wallet approves or rejects the request and returns a `(wallet, error)` tuple — exactly one of the two is `None`: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} wallet, error = await connector.wait_connect() if error: print(f"Connection failed: {error}") else: print(f"Connected to: {connector.app_wallet.name}") print(f"Address: {connector.account.address.to_str(is_bounceable=False)}") print(f"Network: {connector.account.network.name}") ``` After a successful connection, `connector.account` exposes the wallet's `address`, `network`, `public_key`, and `state_init`; `connector.app_wallet` is the matched wallet descriptor. If the user never responds, the pending connect times out after `connect()`'s `timeout` (default `Connector.DEFAULT_CONNECT_TIMEOUT`, 15 minutes) and `wait_connect()` returns `(None, RequestTimeoutError)`. To cancel a pending connect early (for example, when the user closes the dialog), call `connector.drop_connect()`. Complete flow: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID from ton_connect import AppWalletsLoader, FileStorage, TonConnect # Setup constants — see Installation and Initialization MANIFEST_URL = "https://raw.githubusercontent.com/nessshon/ton-connect/main/assets/tonconnect-manifest.json" SESSION_KEY = "user-123" STORAGE_PATH = "./tonconnect-storage.json" async def main() -> None: storage = FileStorage(STORAGE_PATH) app_wallets_loader = AppWalletsLoader(include_wallets=["tonkeeper"]) tc = TonConnect( storage=storage, manifest_url=MANIFEST_URL, app_wallets=app_wallets_loader.get_wallets(), ) connector = tc.create_connector(SESSION_KEY) # Try to restore a previously saved connection before starting a new one restored = await connector.restore() if restored: print("Session restored") print(f"Address: {connector.account.address.to_str(is_bounceable=False)}") else: # Build a connect request with only the address item (no TonProof) request = connector.make_connect_request() # Initiate connection and get the standard tc:// universal link standard_url = await connector.connect( request=request, network=NetworkGlobalID.TESTNET, ) print(f"Connect URL: {standard_url}") # Wallet-specific universal link — opens directly in the target wallet app tonkeeper = app_wallets_loader.get_wallet("tonkeeper") tonkeeper_url = connector.make_connect_url(request, tonkeeper) print(f"Tonkeeper URL: {tonkeeper_url}") # Block until the wallet responds (approve or reject) wallet, error = await connector.wait_connect() if error: print(f"Connection failed: {error}") return print(f"Connected to: {connector.app_wallet.name}") print(f"Address: {connector.account.address.to_str(is_bounceable=False)}") print(f"Network: {connector.account.network.name}") # Close all active bridge connections await tc.close_all() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ## Connecting with TON Proof A plain connection only tells you which address the wallet claims. TON Proof adds cryptographic proof of ownership: your backend issues a challenge payload, the wallet signs it together with your app domain, and you verify the signature after connecting. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from nacl.exceptions import BadSignatureError from ton_core import NetworkGlobalID from ton_connect import ( AppWalletsLoader, FileStorage, TonConnect, VerifyTonProof, create_ton_proof_payload, verify_ton_proof_payload, ) from ton_connect.models import TonProofPayloadDto # Setup constants — see Installation and Initialization MANIFEST_URL = "https://raw.githubusercontent.com/nessshon/ton-connect/main/assets/tonconnect-manifest.json" SESSION_KEY = "user-123" STORAGE_PATH = "./tonconnect-storage.json" # Secret key for HMAC-signing the TonProof challenge payload TON_PROOF_SECRET = "your-secret-key" # Domain your app is served from — must match your tonconnect-manifest.json APP_DOMAIN = "github.com" async def main() -> None: storage = FileStorage(STORAGE_PATH) app_wallets_loader = AppWalletsLoader(include_wallets=["tonkeeper"]) tc = TonConnect( storage=storage, manifest_url=MANIFEST_URL, app_wallets=app_wallets_loader.get_wallets(), ) connector = tc.create_connector(SESSION_KEY) # Step 1: Generate a challenge payload before showing the connect link # ttl: challenge lifetime in seconds — wallet must connect within this window ton_proof_payload = create_ton_proof_payload( secret_key=TON_PROOF_SECRET, ttl=15 * 60, # 15 minutes ) # Step 2: Include the payload in the connect request # This instructs the wallet to return a signed TonProof alongside the address request = connector.make_connect_request(ton_proof_payload=ton_proof_payload) standard_url = await connector.connect( request=request, network=NetworkGlobalID.TESTNET, ) print(f"Connect URL: {standard_url}") tonkeeper = app_wallets_loader.get_wallet("tonkeeper") tonkeeper_url = connector.make_connect_url(request, tonkeeper) print(f"Tonkeeper URL: {tonkeeper_url}") wallet, error = await connector.wait_connect() if error: print(f"Connection failed: {error}") return address = connector.account.address.to_str(is_bounceable=False) print(f"Connected to: {connector.app_wallet.name}") print(f"Address: {address}") print(f"Network: {connector.account.network.name}") try: # Step 3: Verify the challenge payload before checking the proof itself # Ensures the payload was issued by your backend and hasn't expired verify_ton_proof_payload( secret_key=TON_PROOF_SECRET, ton_proof_payload=wallet.ton_proof.payload, ) # Step 4: Verify the TonProof returned by the wallet proof_payload = TonProofPayloadDto( address=wallet.account.address, network=wallet.account.network, public_key=wallet.account.public_key, wallet_state_init=wallet.account.state_init, proof=wallet.ton_proof, ) await VerifyTonProof(proof_payload).verify( allowed_domains=[APP_DOMAIN], valid_auth_time=15 * 60, # 15 minutes ) print("TonProof verified") except BadSignatureError as e: print(f"TonProof verification failed: {e}") # Disconnect the wallet if the proof is invalid await connector.disconnect() # Close all active bridge connections await tc.close_all() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` Both verification steps raise `nacl.exceptions.BadSignatureError` on failure, so wrap them in a single `try/except` and disconnect the wallet if the proof does not check out. `VerifyTonProof(...).verify()` performs five cryptographic checks — the full list, and how to verify a proof received from a frontend as a raw dict, is in [Backend Verification](/ton-connect/backend-verification#verifying-ton-proof). `TON_PROOF_SECRET` must never leave your backend — it is what proves the challenge was issued by you. ## Disconnecting `disconnect()` notifies the wallet, removes the session from storage, and closes the bridge connection. It raises `WalletNotConnectedError` if no wallet is connected: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_connect.exceptions import WalletNotConnectedError try: await connector.disconnect() except WalletNotConnectedError: print("No wallet connected") ``` ## Importing a Connection A session established by a JS frontend using TonConnect UI can be taken over by the Python backend — no new connect flow, no second wallet approval. TonConnect UI keeps its session under two `localStorage` keys: | Key | Contents | | - | - | | `ton-connect-storage_bridge-connection` | A dict with the connect event payload, session key pair, and bridge URL. | | `ton-connect-storage_http-bridge-gateway` | The `lastEventId` of the SSE stream. | `lastEventId` is optional in the `ActiveConnection` model, but it is needed to resume the SSE stream where it left off and avoid replaying old events — merge it into the connection dict before importing. Validate the merged dict into a typed `ActiveConnection` and reconstruct a connector with `tc.from_connection(...)`, which stores the connection under the given session key and restores the wallet session: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_connect import AppWalletsLoader, FileStorage, TonConnect from ton_connect.models import ActiveConnection # Setup constants — see Installation and Initialization MANIFEST_URL = "https://raw.githubusercontent.com/nessshon/ton-connect/main/assets/tonconnect-manifest.json" SESSION_KEY = "user-123" STORAGE_PATH = "./tonconnect-storage.json" # Value of the ton-connect-storage_bridge-connection key ("..." shortened) BRIDGE_CONNECTION = { "type": "http", "connectEvent": { "id": 1771782910, "event": "connect", "payload": {"items": ["..."], "device": {"...": "..."}}, }, "session": { "sessionKeyPair": {"publicKey": "...", "secretKey": "..."}, "walletPublicKey": "...", "bridgeUrl": "https://bridge.tonapi.io/bridge", }, "lastWalletEventId": 1771782910, "nextRpcRequestId": 0, } # Value of the ton-connect-storage_http-bridge-gateway key BRIDGE_LAST_EVENT_ID = "1770180433054700" async def main() -> None: tc = TonConnect( storage=FileStorage(STORAGE_PATH), manifest_url=MANIFEST_URL, app_wallets=AppWalletsLoader(include_wallets=["tonkeeper"]).get_wallets(), ) # Merge the SSE stream position into the connection dict connection = {**BRIDGE_CONNECTION, "lastEventId": BRIDGE_LAST_EVENT_ID} active_connection = ActiveConnection.model_validate(connection) # Reconstruct a connector from the existing connection — no new connect flow connector = await tc.from_connection(active_connection, SESSION_KEY) print(f"Address: {connector.account.address.to_str(is_bounceable=False)}") await tc.close_all() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` From here the connector behaves like any other connected session — `connector.account`, `send_transaction`, `sign_data`, and `disconnect` all work as usual. ## Exporting a Connection The inverse operation returns the stored session as an `ActiveConnection`, or `None` if there is no active connection: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} active_connection = await connector.to_connection() ``` To hand the session to another process or store it elsewhere, serialize it with `active_connection.dump()` — it uses the camelCase aliases of the TonConnect UI storage format (`connectEvent`, `lastEventId`, ...), not the Python field names. Full source code of the connect flows on GitHub. Full import example with real exported storage values. # Errors Source: https://tonutils.ness.uz/ton-connect/errors Reference of TonConnect exception types and wallet RPC error codes. All exceptions derive from `TonConnectError` (importable from `ton_connect.exceptions`), which carries `.code` (numeric RPC error code, `0` unless mapped), `.message` (description), and `.info` (extended explanation, or `None`). Errors surface in two ways: as the `error` element of the tuples returned by `wait_connect()` / `wait_transaction()` / `wait_sign_data()` and the `error` argument of [event handlers](/ton-connect/event-handling), or raised directly by methods such as `connect()` and `disconnect()`. ## Connection and Feature Errors Raised or delivered by the library itself: | Exception | Description | | - | - | | `WalletAlreadyConnectedError` | `connect()` was called while a wallet is already connected. Disconnect first. | | `WalletNotConnectedError` | `send_transaction()`, `sign_data()`, or `disconnect()` was called while no wallet is connected. | | `WalletMissingRequiredFeaturesError` | The connected wallet does not declare the minimum required features. Delivered as the `error` of the connect result. | | `WalletNotSupportFeatureError` | The wallet does not support the requested feature — for example, more messages than its `maxMessages`, extra currencies, or the requested sign-data type. | | `WalletWrongNetworkError` | The wallet is connected to a different network than the one passed to `connect(network=...)`. Delivered as the `error` of the connect result. | | `FetchWalletsError` | `AppWalletsLoader` could not fetch or parse the wallets list. | ## Wallet Error Codes Errors reported by the wallet over the bridge are mapped from their RPC code to typed exceptions: | Code | Exception | Meaning | | - | - | - | | 0 | `UnknownError` | An unknown error occurred in the wallet while processing the request. | | 1 | `BadRequestError` | The request is malformed or contains invalid parameters. | | 2 | `ManifestNotFoundError` | The wallet could not fetch `tonconnect-manifest.json` at the given `manifest_url`. | | 3 | `ManifestContentError` | The manifest content is invalid or improperly formatted. | | 100 | `UnknownAppError` | The app sent an RPC request while not connected to the wallet. | | 300 | `UserRejectsError` | The user declined the connection or rejected the request in the wallet. | | 400 | `MethodNotSupportedError` | The wallet does not support the requested RPC method. | | 500 | `RequestTimeoutError` | The user did not respond within the time limit. Also produced locally when a connect or request timeout expires. | Unmapped codes produce a plain `TonConnectError` with `.code` set. To map a code yourself, `TonConnectErrors.from_code(code, message)` returns the corresponding typed exception instance. ## Handling Errors Check the `error` element of the result tuple and branch on the exception type: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_connect.exceptions import RequestTimeoutError, UserRejectsError request_id = await connector.send_transaction(payload) result, error = await connector.wait_transaction(request_id) if error is None: print(f"Transaction hash: {result.normalized_hash}") elif isinstance(error, UserRejectsError): print("User rejected the transaction.") elif isinstance(error, RequestTimeoutError): print("User did not respond in time.") else: print(f"Transaction failed [{error.code}]: {error.message}") ``` The same pattern applies to `wait_connect()` and `wait_sign_data()`, and to the `error` argument of event handlers. Backend verification failures (`VerifyTonProof`, `VerifySignData`, `verify_ton_proof_payload`) raise `nacl.exceptions.BadSignatureError`, not `TonConnectError`. See [Backend Verification](/ton-connect/backend-verification). # Event Handling Source: https://tonutils.ness.uz/ton-connect/event-handling React to wallet connections, transactions, and errors with registered event handlers instead of awaiting results inline. Besides awaiting results inline with `wait_connect()` / `wait_transaction()` / `wait_sign_data()`, the library can dispatch results to registered handlers — the natural style for long-lived services such as Telegram bots, where many users hold sessions concurrently. Both styles work at the same time: every result resolves the pending `wait_*` call and dispatches the corresponding event. ## Event Types Events are members of a single `Event` enum: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_connect import Event ``` | Event | Fires when | | - | - | | `Event.CONNECT` | A connect attempt resolves: the wallet approved the connection (`error` is `None`), or it failed — user rejection, wrong network, missing required features, or timeout (`error` is set). | | `Event.DISCONNECT` | The wallet reports a disconnect over the bridge — for example, the user disconnects from the wallet side (`error` is `None`) or the wallet sends a disconnect error (`error` is set). Calling `connector.disconnect()` does not dispatch this event. | | `Event.TRANSACTION` | A `send_transaction` request resolves: signed and submitted (`result` is set) or rejected/timed out (`error` is set). | | `Event.SIGN_DATA` | A `sign_data` request resolves: signed (`result` is set) or rejected/timed out (`error` is set). | | `Event.MESSAGE` | Any raw wallet message arrives, before the connector processes it — connect and disconnect events and RPC responses alike. Useful for logging and debugging. | | `Event.ERROR` | A bridge/provider-level error occurs, or an exception is raised inside another event handler. | There is no separate error-event enum. Errors that belong to a specific action are delivered as the `error` argument of that action's handler (`CONNECT`, `DISCONNECT`, `TRANSACTION`, `SIGN_DATA`); `Event.ERROR` only receives errors that cannot be attributed to a pending request. ## Registering Handlers Register a handler with the `@tc.on(...)` decorator or the `tc.register(...)` method. One handler is stored per event — registering again replaces the previous handler. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_connect import Connector, Event, TonConnect from ton_connect.exceptions import TonConnectError tc = TonConnect(...) @tc.on(Event.CONNECT) async def on_connect(connector: Connector, error: TonConnectError | None) -> None: ... # Equivalent without the decorator: tc.register(Event.CONNECT, on_connect) ``` Register all handlers **before** calling `tc.create_connector(...)`. Handlers are copied into each connector at creation time, so handlers registered afterwards do not apply to connectors that already exist. Each event has a fixed handler signature. The first argument is always the `Connector` that dispatched the event: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} @tc.on(Event.CONNECT) async def on_connect(connector: Connector, error: TonConnectError | None) -> None: ... @tc.on(Event.DISCONNECT) async def on_disconnect(connector: Connector, error: TonConnectError | None) -> None: ... @tc.on(Event.TRANSACTION) async def on_transaction( connector: Connector, request_id: int, result: SendTransactionResult | None, error: TonConnectError | None, ) -> None: ... @tc.on(Event.SIGN_DATA) async def on_sign_data( connector: Connector, request_id: int, result: SignDataResult | None, error: TonConnectError | None, ) -> None: ... @tc.on(Event.MESSAGE) async def on_message(connector: Connector, message: WalletMessage) -> None: ... @tc.on(Event.ERROR) async def on_error(connector: Connector, error: TonConnectError) -> None: ... ``` `SendTransactionResult`, `SignDataResult`, and `WalletMessage` are importable from `ton_connect.models`. For `TRANSACTION` and `SIGN_DATA`, exactly one of `result` and `error` is set. An exception raised inside a handler is caught, wrapped in `TonConnectError` if needed, and dispatched to the `Event.ERROR` handler. Exceptions raised inside the `ERROR` handler itself are swallowed. ## Passing Context Handlers receive only the connector and event data, so application state (database sessions, user objects) is attached as context. Context is a plain key-value store, readable and writable through item access. Global context — shared by all connectors: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} tc = TonConnect( storage=storage, manifest_url=MANIFEST_URL, app_wallets=app_wallets, db=db, # any extra keyword arguments become context ) tc["greeting"] = "Hello" # or set keys later ``` Per-connector context — merged on top of the global context at creation: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} connector = tc.create_connector(session_key, user_id=123) ``` Inside a handler, read context from the connector: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} @tc.on(Event.CONNECT) async def on_connect(connector: Connector, error: TonConnectError | None) -> None: user_id = connector["user_id"] db = connector["db"] ``` Connectors also support item assignment (`connector["key"] = value`) to update their own context. Accessing a missing key raises `KeyError`. ## Connection Lifecycle | Method | Description | | - | - | | `await connector.pause()` | Pause SSE listening on the bridge without dropping the session. | | `await connector.unpause()` | Resume SSE listening. | | `await connector.close()` | Close the connector's bridge connections and cancel pending requests, without notifying the wallet. The session in storage stays intact. | | `await tc.close_all()` | Close all active connectors and clear the connector registry. | | `tc.connectors` | Read-only copy of the active connectors keyed by session key. | | `connector.resume_connect(timeout)` | After a process restart, re-create the pending connect future so `wait_connect()` works again. `timeout` is the remaining time in seconds. | | `connector.resume_request(request_id, event, timeout)` | After a process restart, re-create a pending request future (`event` is `Event.TRANSACTION` or `Event.SIGN_DATA`) so `wait_transaction()` / `wait_sign_data()` work again. | | `await connector.to_connection()` | Export the stored session as an `ActiveConnection`, or `None` if there is no active connection. See [Exporting a Connection](/ton-connect/connect-wallet#exporting-a-connection). | `resume_connect` and `resume_request` make pending requests restart-safe: persist the `request_id`, event type, and deadline before your process exits, then re-create the connector, call `await connector.restore()`, and resume waiting. ## Complete Example A minimal event-driven flow: the `CONNECT` handler sends a transaction as soon as the wallet connects, the `TRANSACTION` handler reports the result, and the `ERROR` handler logs anything unattributed. ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} import asyncio from ton_core import Address, to_nano from ton_connect import AppWalletsLoader, Connector, Event, FileStorage, TonConnect from ton_connect.exceptions import TonConnectError from ton_connect.models import ( SendTransactionMessage, SendTransactionPayload, SendTransactionResult, ) MANIFEST_URL = "https://raw.githubusercontent.com/nessshon/ton-connect/main/assets/tonconnect-manifest.json" SESSION_KEY = "user-123" STORAGE_PATH = "./tonconnect-storage.json" DESTINATION_ADDRESS = Address("UQ...") TRANSFER_AMOUNT = to_nano(0.01) storage = FileStorage(STORAGE_PATH) app_wallets_loader = AppWalletsLoader(include_wallets=["tonkeeper"]) tc = TonConnect( storage=storage, manifest_url=MANIFEST_URL, app_wallets=app_wallets_loader.get_wallets(), ) # Signals main() that the flow finished done = asyncio.Event() @tc.on(Event.CONNECT) async def on_connect(connector: Connector, error: TonConnectError | None) -> None: if error: print(f"Connection failed: {error}") done.set() return address = connector.account.address.to_str(is_bounceable=False) print(f"Connected: {address}") payload = SendTransactionPayload( messages=[ SendTransactionMessage( address=DESTINATION_ADDRESS, amount=TRANSFER_AMOUNT, payload="Hello from ton-connect!", ), ], ) await connector.send_transaction(payload) print("Transaction sent, approve it in the wallet...") @tc.on(Event.TRANSACTION) async def on_transaction( connector: Connector, request_id: int, result: SendTransactionResult | None, error: TonConnectError | None, ) -> None: if error: print(f"Transaction failed: {error}") else: print(f"Transaction hash: {result.normalized_hash}") done.set() @tc.on(Event.ERROR) async def on_error(connector: Connector, error: TonConnectError) -> None: print(f"Error: {error}") async def main() -> None: # Handlers above are already registered, so the connector inherits them connector = tc.create_connector(SESSION_KEY) if not await connector.restore(): request = connector.make_connect_request() url = await connector.connect(request=request) print(f"Connect URL: {url}") else: # Session already active — trigger the flow manually await on_connect(connector, None) await done.wait() await tc.close_all() if __name__ == "__main__": asyncio.run(main()) ``` Full runnable scripts for all TON Connect flows on GitHub. # TON Connect Source: https://tonutils.ness.uz/ton-connect/index Python SDK for the TON Connect protocol — wallet connections, transactions, data signing, and proof verification. **ton-connect** is a Python SDK implementing the [TON Connect](https://github.com/ton-connect) protocol for dApp backends and bots. It handles wallet connections over the HTTP bridge, transaction and sign-data requests, and cryptographic verification of wallet responses — everything a server-side application needs to talk to TON wallets. The package is standalone and does not require `tonutils`. It replaces the former `tonutils.tonconnect` module. ## Features * **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** — file, in-memory, or any custom key-value backend. * **TonConnect-UI Interop** — import and export sessions created by TonConnect-UI frontends. ## Installation ```bash theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} pip install ton-connect ``` | Requirement | Version | | - | - | | Python | 3.10+ | ## Quick Start ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} import asyncio from ton_core import NetworkGlobalID from ton_connect import AppWalletsLoader, FileStorage, TonConnect MANIFEST_URL = "https://example.com/tonconnect-manifest.json" SESSION_KEY = "user-123" async def main() -> None: storage = FileStorage("./tonconnect-storage.json") loader = AppWalletsLoader(include_wallets=["tonkeeper"]) tc = TonConnect( storage=storage, manifest_url=MANIFEST_URL, app_wallets=loader.get_wallets(), ) connector = tc.create_connector(SESSION_KEY) # Resume a previously saved session if one exists restored = await connector.restore() if not restored: request = connector.make_connect_request() url = await connector.connect(request=request, network=NetworkGlobalID.TESTNET) print(f"Connect URL: {url}") wallet, error = await connector.wait_connect() if error: print(f"Connection failed: {error}") return print(f"Address: {connector.account.address.to_str(is_bounceable=False)}") await tc.close_all() if __name__ == "__main__": asyncio.run(main()) ``` ## Guide Manifest, storage backends, wallet catalogue, and the TonConnect manager. Connect links, TON Proof, session restore, import, and export. Build transaction payloads and handle wallet responses. Request signatures over text, binary, and cell payloads. ## Reference * [Event Handling](/ton-connect/event-handling) — react to connect, transaction, and sign-data events with registered handlers. * [Backend Verification](/ton-connect/backend-verification) — verify TON Proof and sign-data results server-side. * [Errors](/ton-connect/errors) — exception hierarchy and wallet RPC error codes. Source code, examples, and issue tracker. # TON Connect Installation and Initialization Source: https://tonutils.ness.uz/ton-connect/installation-and-initialization Install ton-connect, create the manifest, choose a storage backend, load wallet apps, and set up the TonConnect manager. Everything needed before connecting a wallet: package installation, the application manifest, session storage, the wallet catalogue, and the `TonConnect` manager with per-user connectors. All other pages assume this setup. ## Installation ```bash theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} pip install ton-connect ``` Requires Python 3.10 or later. ## Creating the Manifest `tonconnect-manifest.json` describes your application to the wallet. When a user connects, the wallet fetches this file to display your app's name and icon on the confirmation screen. Create it according to the [official guidelines](https://docs.ton.org/applications/ton-connect/core-concepts#manifest) and host it at a publicly reachable HTTPS URL: ```json theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} { "url": "https://example.com", "name": "Example App", "iconUrl": "https://example.com/icon.png", "termsOfUseUrl": "https://example.com/terms", "privacyPolicyUrl": "https://example.com/privacy" } ``` `url`, `name`, and `iconUrl` are required; `termsOfUseUrl` and `privacyPolicyUrl` are optional. The manifest domain is what the user sees in the wallet and what backend verification checks against: TON Proof and sign-data results carry the app domain, and [Backend Verification](/ton-connect/backend-verification) validates it against your allowlist. ## Storage Session data is persisted through a small key-value interface, `StorageProtocol`: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} class StorageProtocol(t.Protocol): async def set_item(self, key: str, value: t.Any) -> None: ... async def get_item(self, key: str) -> t.Any | None: ... async def remove_item(self, key: str) -> None: ... ``` It is a `typing.Protocol` — any object with these three async methods works, no subclassing required. Values are arbitrary JSON-serializable objects (dicts), not pre-encoded strings. Two implementations ship with the package: * `FileStorage(path)` — persists sessions to a JSON file. Suitable for single-process apps; sessions survive restarts. * `MemoryStorage()` — keeps sessions in a plain dict. Suitable for tests and short-lived scripts; all sessions are lost on restart. For distributed or database-backed setups, implement the protocol over your own store: A Redis-backed implementation (the `redis` part is illustrative — any async key-value store works the same way): ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} import json import typing as t import redis.asyncio as redis class RedisStorage: def __init__(self, client: redis.Redis, prefix: str = "tonconnect:") -> None: self.client = client self.prefix = prefix async def set_item(self, key: str, value: t.Any) -> None: await self.client.set(self.prefix + key, json.dumps(value)) async def get_item(self, key: str) -> t.Any | None: raw = await self.client.get(self.prefix + key) return json.loads(raw) if raw is not None else None async def remove_item(self, key: str) -> None: await self.client.delete(self.prefix + key) ``` Serialize values yourself if the store only accepts strings — the library passes and expects plain Python objects. ## Loading Wallet Apps `AppWalletsLoader` fetches the wallet catalogue (Tonkeeper, MyTonWallet, etc.) from the TON registry, with a memory cache and an optional local file fallback: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_connect import AppWalletsLoader loader = AppWalletsLoader(include_wallets=["tonkeeper"]) app_wallets = loader.get_wallets() ``` | Parameter | Description | | - | - | | `source` | Remote URL or local path to the wallets JSON (default: `https://config.ton.org/wallets-v2.json`). | | `include_wallets` | App names to include, or `None` for all. | | `exclude_wallets` | App names to exclude, or `None`. | | `order_wallets` | Desired ordering of app names, or `None`. | | `require_sign_data` | If `True`, only include wallets that support the `SignData` feature. | | `fallback_path` | Local fallback file path, or `None`. Fetched data is saved here and reloaded when the remote source is unreachable. | | `cache_ttl` | Cache lifetime in seconds, or `None` for no expiry. | | `timeout` | HTTP fetch timeout in seconds (default: `5.0`). | `get_wallets()` is synchronous and returns `list[AppWallet]`; `get_wallet(app_name)` returns a single descriptor or `None`. Only wallets with an SSE bridge URL are kept — others cannot receive requests from a backend. If the remote source fails and no usable fallback exists, `FetchWalletsError` is raised. Set `fallback_path` in production so a registry outage does not prevent your app from starting. ## Initializing TON Connect `TonConnect` is the manager that holds shared configuration and creates per-user connectors: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_connect import AppWalletsLoader, FileStorage, TonConnect MANIFEST_URL = "https://example.com/tonconnect-manifest.json" storage = FileStorage("./tonconnect-storage.json") loader = AppWalletsLoader(include_wallets=["tonkeeper"]) tc = TonConnect( storage=storage, manifest_url=MANIFEST_URL, app_wallets=loader.get_wallets(), ) ``` | Parameter | Description | | - | - | | `storage` | Key-value storage backend implementing `StorageProtocol`. | | `manifest_url` | Publicly reachable URL of your `tonconnect-manifest.json`. | | `app_wallets` | Wallet descriptors (`list[AppWallet]`) used as bridge connection sources. | | `headers` | Extra HTTP headers for bridge requests, or `None`. | | `**context` | Arbitrary context values, available in event handlers via `connector[key]`. | ## Creating a Connector Each user session gets its own `Connector` with an isolated storage namespace: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} connector = tc.create_connector(SESSION_KEY) restored = await connector.restore() ``` * `create_connector(session_key)` is synchronous. `session_key` is the per-user identity — any unique string, typically a Telegram user id (`str(user_id)`). * `await connector.restore()` resumes a previously saved session from storage; it returns `True` if an active connection was found. Call it before starting a new connection flow. * `tc.connectors` is a read-only dict of active connectors keyed by session key. * On application shutdown, call `await tc.close_all()` to close all bridge connections. Continue with [Connect Wallet](/ton-connect/connect-wallet) to build the connection flow. Full runnable scripts: connect, transactions, sign data, and verification. # Send Transaction Source: https://tonutils.ness.uz/ton-connect/send-transaction Request the connected wallet to sign and broadcast transactions, and track the result. A connected wallet can be asked to sign and broadcast transactions: build a typed payload, send it over the bridge, and the user approves it in their wallet app. All snippets assume a connected `connector` — see [Connect Wallet](/ton-connect/connect-wallet). ## Building the Request A request is a `SendTransactionPayload` containing one or more `SendTransactionMessage` entries: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, to_nano from ton_connect.models import SendTransactionMessage, SendTransactionPayload payload = SendTransactionPayload( messages=[ SendTransactionMessage( address=Address("UQ..."), amount=to_nano(0.01), payload="Hello from ton-connect!", ), ], ) ``` `SendTransactionMessage` fields: | Field | Description | | - | - | | `address` | Destination address — `Address` or raw string. | | `amount` | Transfer amount in nanotons (`int`; 1 TON = 1,000,000,000 nanotons). | | `payload` | Optional message body. Pass a `Cell` for structured bodies (jetton transfers, contract calls) or a `str`, which is automatically wrapped as a text comment. | | `state_init` | Optional contract `StateInit`, e.g. when the transfer deploys a contract. | | `extra_currency` | Optional extra currency map (`dict[int, str]`). Requires wallet support. | `SendTransactionPayload` fields: | Field | Description | | - | - | | `network` | Target network. Defaults to the network passed to `connect()`. | | `from_address` | Sender address override. Defaults to the connected account's address. | | `valid_until` | Unix timestamp after which the wallet rejects the transaction. Defaults to now + 5 minutes; also used as the request timeout, and the wallet UI shows it as a countdown. | | `messages` | List of outgoing messages. | ## Sending `send_transaction()` sends the request to the wallet and returns a `request_id` immediately — the user still has to approve it in their wallet app: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} request_id = await connector.send_transaction(payload) ``` Before sending, the connector automatically verifies that the connected wallet declares the `SendTransaction` feature, supports the number of messages in the payload (each wallet advertises a `max_messages` limit, typically 4), and supports extra currencies if any message uses them. A failed check raises `WalletNotSupportFeatureError`; calling without a connected wallet raises `WalletNotConnectedError`. ## Waiting for the Result `wait_transaction(request_id)` blocks until the wallet signs and submits the transaction (or rejects it, or the request times out) and returns a `(result, error)` tuple: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} result, error = await connector.wait_transaction(request_id) if error: print(f"Transaction failed: {error}") else: print(f"Transaction boc : {result.boc}") print(f"Transaction hash: {result.normalized_hash}") ``` `result.boc` is the signed external message BoC returned by the wallet. `result.normalized_hash` is the locally computed normalized external-message hash — it can be used to look the transaction up in explorers and indexers, but it is not the on-chain transaction hash. Common errors are `UserRejectsError` (the user declined in the wallet UI) and `RequestTimeoutError` (no response before `valid_until`) — see [Errors](/ton-connect/errors) for the full list. To stop waiting for a pending request (for example, when the user navigates away), call `connector.drop_request(request_id)`. ## Complete Example ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Address, NetworkGlobalID, to_nano from ton_connect import AppWalletsLoader, FileStorage, TonConnect from ton_connect.models import ( SendTransactionMessage, SendTransactionPayload, ) # Setup constants — see Installation and Initialization MANIFEST_URL = "https://raw.githubusercontent.com/nessshon/ton-connect/main/assets/tonconnect-manifest.json" SESSION_KEY = "user-123" STORAGE_PATH = "./tonconnect-storage.json" # Recipient, optional text comment, and amount in nanotons DESTINATION_ADDRESS = Address("UQCDrgGaI6gWK-qlyw69xWZosurGxrpRgIgSkVsgahUtxZR0") TRANSFER_COMMENT = "Hello from ton-connect!" TRANSFER_AMOUNT = to_nano(0.01) async def main() -> None: storage = FileStorage(STORAGE_PATH) app_wallets_loader = AppWalletsLoader(include_wallets=["tonkeeper"]) tc = TonConnect( storage=storage, manifest_url=MANIFEST_URL, app_wallets=app_wallets_loader.get_wallets(), ) connector = tc.create_connector(SESSION_KEY) # Try to restore a previously saved connection before starting a new one restored = await connector.restore() if not restored: request = connector.make_connect_request() await connector.connect( request=request, network=NetworkGlobalID.TESTNET, ) tonkeeper = app_wallets_loader.get_wallet("tonkeeper") tonkeeper_url = connector.make_connect_url(request, tonkeeper) print(f"Tonkeeper URL: {tonkeeper_url}") wallet, error = await connector.wait_connect() if error: print(f"Connection failed: {error}") return address = connector.account.address.to_str(is_bounceable=False) print(f"Address: {address}") # Build the transaction payload payload = SendTransactionPayload( messages=[ SendTransactionMessage( address=DESTINATION_ADDRESS, amount=TRANSFER_AMOUNT, payload=TRANSFER_COMMENT, ), ], ) # Send the transaction request to the connected wallet # Returns a request_id immediately — the user approves in their wallet app request_id = await connector.send_transaction(payload) print("Transaction sent, waiting for confirmation...") # Block until the wallet signs and submits the transaction (or rejects/times out) result, error = await connector.wait_transaction(request_id) if error: print(f"Transaction failed: {error}") else: print(f"Transaction boc : {result.boc}") print(f"Transaction hash: {result.normalized_hash}") # Close all active bridge connections await tc.close_all() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` ## Batch Transfers A single request can contain several messages — the wallet signs and sends them in one transaction. Add more `SendTransactionMessage` entries to the `messages` list: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} payload = SendTransactionPayload( messages=[ SendTransactionMessage(address=Address("UQ..."), amount=to_nano(0.01)), SendTransactionMessage(address=Address("UQ..."), amount=to_nano(0.02)), SendTransactionMessage(address=Address("UQ..."), amount=to_nano(0.03)), ], ) ``` The number of messages is limited by the wallet's advertised `max_messages`, checked by `send_transaction()` as described in [Sending](#sending). Full source code of the transaction flow on GitHub # Sign Data Source: https://tonutils.ness.uz/ton-connect/sign-data Request the connected wallet to sign off-chain data and verify the returned signature. The `signData` method asks the connected wallet to sign arbitrary off-chain content — recording agreement to terms of service, authorizing an off-chain action — and returns an Ed25519 signature you can verify and store; nothing is sent on-chain. All snippets assume a connected `connector` — see [Connect Wallet](/ton-connect/connect-wallet). ## Payload Types Three payload models are supported: | Payload | Fields | Use for | | - | - | - | | `SignDataPayloadText` | `text` | Human-readable UTF-8 text shown to the user in the wallet UI. | | `SignDataPayloadBinary` | `raw_bytes` | Opaque binary data. | | `SignDataPayloadCell` | `tlb_schema`, `cell` | Structured on-chain-style data described by a TL-B schema. | ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import Cell from ton_connect.models import ( SignDataPayloadBinary, SignDataPayloadCell, SignDataPayloadText, ) text_payload = SignDataPayloadText(text="I agree to the Terms of Service") binary_payload = SignDataPayloadBinary(raw_bytes=b"arbitrary bytes") cell_payload = SignDataPayloadCell( tlb_schema="message#_ text:string = Message;", cell=Cell.one_from_boc("te6..."), ) ``` Wallets advertise which payload types they support as part of their `SignData` feature. `sign_data()` automatically verifies that the connected wallet declares the feature and supports the payload's type, raising `WalletNotSupportFeatureError` otherwise. ## Requesting a Signature `sign_data(payload)` sends the request and returns a `request_id` immediately; `wait_sign_data(request_id)` blocks until the wallet responds and returns a `(result, error)` tuple: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_connect.models import SignDataPayloadText payload = SignDataPayloadText(text="I agree to the Terms of Service") request_id = await connector.sign_data(payload) result, error = await connector.wait_sign_data(request_id) if error: print(f"Signing failed: {error}") else: print(f"Signature: {result.signature.as_hex}") print(f"Signed at: {result.timestamp}") ``` The result fields: | Field | Description | | - | - | | `signature` | Ed25519 signature (64 bytes); `signature.as_hex` gives the hex string. | | `address` | Signer wallet address. | | `timestamp` | Unix timestamp when the wallet produced the signature. | | `domain` | App domain the wallet signed for. | | `payload` | The payload the user signed. | Common errors are `UserRejectsError` and `RequestTimeoutError` — see [Errors](/ton-connect/errors). A pending request can be canceled with `connector.drop_request(request_id)`. ## Verifying the Signature Never trust a signature without verifying it. Build a `SignDataPayloadDto` from the connected account and the result, then run `VerifySignData`. Any failed check raises `nacl.exceptions.BadSignatureError`: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from nacl.exceptions import BadSignatureError from ton_connect import VerifySignData from ton_connect.models import SignDataPayloadDto try: sign_data_payload = SignDataPayloadDto( address=connector.account.address, network=connector.account.network, public_key=connector.account.public_key, wallet_state_init=connector.account.state_init, signature=result.signature, timestamp=result.timestamp, domain=result.domain, payload=result.payload, ) await VerifySignData(sign_data_payload).verify( allowed_domains=["your-app-domain.com"], valid_auth_time=5 * 60, # 5 minutes ) print("SignData verified") except BadSignatureError as e: print(f"SignData verification failed: {e}") ``` Verification performs five cryptographic checks — the full list, and how to verify a signature received from a frontend as a raw dict, is in [Backend Verification](/ton-connect/backend-verification#verifying-signed-data). ## Complete Example ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from nacl.exceptions import BadSignatureError from ton_core import NetworkGlobalID from ton_connect import AppWalletsLoader, FileStorage, TonConnect, VerifySignData from ton_connect.models import ( SignDataPayloadDto, SignDataPayloadText, ) # Setup constants — see Installation and Initialization MANIFEST_URL = "https://raw.githubusercontent.com/nessshon/ton-connect/main/assets/tonconnect-manifest.json" SESSION_KEY = "user-123" STORAGE_PATH = "./tonconnect-storage.json" # Domain your app is served from — must match your tonconnect-manifest.json APP_DOMAIN = "github.com" # Text the user will be asked to sign in their wallet UI SIGN_TEXT = "I agree to the Terms of Service" async def main() -> None: storage = FileStorage(STORAGE_PATH) app_wallets_loader = AppWalletsLoader(include_wallets=["tonkeeper"]) tc = TonConnect( storage=storage, manifest_url=MANIFEST_URL, app_wallets=app_wallets_loader.get_wallets(), ) connector = tc.create_connector(SESSION_KEY) # Try to restore a previously saved connection before starting a new one restored = await connector.restore() if not restored: request = connector.make_connect_request() await connector.connect( request=request, network=NetworkGlobalID.TESTNET, ) tonkeeper = app_wallets_loader.get_wallet("tonkeeper") tonkeeper_url = connector.make_connect_url(request, tonkeeper) print(f"Tonkeeper URL: {tonkeeper_url}") wallet, error = await connector.wait_connect() if error: print(f"Connection failed: {error}") return address = connector.account.address.to_str(is_bounceable=False) print(f"Address: {address}") # Build the sign data payload payload = SignDataPayloadText(text=SIGN_TEXT) # Send the sign data request to the connected wallet # Returns a request_id immediately — the user approves in their wallet app request_id = await connector.sign_data(payload) print("Sign request sent, waiting for confirmation...") # Block until the wallet signs the data (or rejects/times out) result, error = await connector.wait_sign_data(request_id) if error: print(f"Signing failed: {error}") return print(f"Signature: {result.signature.as_hex}") print(f"Signed at: {result.timestamp}") try: # Verify the signature returned by the wallet sign_data_payload = SignDataPayloadDto( address=connector.account.address, network=connector.account.network, public_key=connector.account.public_key, wallet_state_init=connector.account.state_init, signature=result.signature, timestamp=result.timestamp, domain=result.domain, payload=result.payload, ) await VerifySignData(sign_data_payload).verify( allowed_domains=[APP_DOMAIN], valid_auth_time=5 * 60, # 5 minutes ) print("SignData verified") except BadSignatureError as e: print(f"SignData verification failed: {e}") # Close all active bridge connections await tc.close_all() if __name__ == "__main__": import asyncio asyncio.run(main()) ``` Full source code of the sign data flow on GitHub # Block Scanner Source: https://tonutils.ness.uz/tools/block-scanner Subscribe to new TON blocks and transactions in real time over a lite client, with resumable scanning. `BlockScanner` from `tonutils.tools.block_scanner` follows the masterchain, discovers every new shard block by walking back from the shard tips each masterchain block references, and emits typed events for each block and its transactions. Use it to build indexers, payment trackers, or any service that needs a live stream of on-chain activity. ## Requirements The scanner works over the native ADNL protocol and accepts only a `LiteClient` or `LiteBalancer` — HTTP clients are not supported. Connect the client before starting the scanner: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} from ton_core import NetworkGlobalID from tonutils.clients import LiteBalancer from tonutils.types import DEFAULT_ADNL_RETRY_POLICY client = LiteBalancer.from_network_config( network=NetworkGlobalID.MAINNET, rps_limit=100, retry_policy=DEFAULT_ADNL_RETRY_POLICY, ) ``` Public lite servers may be unstable under load. For continuous scanning, use a private lite-server config with `LiteBalancer.from_config`. Reduce `rps_limit` if you hit rate limits; increase it for faster scanning on dedicated nodes. ## Storage To survive restarts, give the scanner a storage object that persists the last processed masterchain seqno. Storage is any object matching `BlockScannerStorageProtocol` — two async methods: | Method | Description | | - | - | | `get_mc_seqno()` | Return the last processed masterchain seqno, or `None`. | | `set_mc_seqno(seqno)` | Persist the last processed masterchain seqno. | The scanner calls `set_mc_seqno` after each fully processed masterchain block; `resume()` reads the saved value with `get_mc_seqno` and continues from the next block. A minimal file-backed implementation: ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} 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") ``` Any backend works — a database row or a Redis key implement the same two methods. Storage is optional; without it the scanner runs, but `resume()` raises `RuntimeError`. ## Handlers Create the scanner and register handlers with decorators (or pass them as `on_block=` / `on_transactions=` / `on_error=` constructor arguments): ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} scanner = BlockScanner(client, storage=storage) @scanner.on_block() async def handle_block(event: BlockEvent) -> None: ... @scanner.on_transactions() async def handle_transactions(event: TransactionsEvent) -> None: ... @scanner.on_error() async def handle_error(event: ErrorEvent) -> None: ... ``` For each shard block, `BlockEvent` is emitted first, then `TransactionsEvent` with that block's transactions. Transactions are fetched only when an `on_transactions` handler is registered. Exceptions raised inside `on_block` / `on_transactions` handlers are routed to the error handler; the `on_error` handler must never raise — exceptions inside it are silently dropped. All events are frozen dataclasses sharing three base fields: | Field | Type | Description | | - | - | - | | `client` | `LiteBalancer \| LiteClient` | The client the scanner runs on. | | `mc_block` | `BlockIdExt` | Masterchain block being processed. | | `context` | `Mapping[str, Any]` | Shared user context (extra keyword arguments passed to `BlockScanner(...)`). | Event-specific fields: **`BlockEvent`** — a shard block discovered by the scanner: * `block` — shard block identifier (`BlockIdExt` with `workchain`, `shard`, `seqno`, `root_hash`, `file_hash`). **`TransactionsEvent`** — transactions fetched for a shard block: * `block` — shard block identifier (`BlockIdExt`). * `transactions` — list of `Transaction` objects from this block. **`ErrorEvent`** — an error raised during scanning or handler execution: * `error` — the raised exception. * `event` — the related event, or `None`. * `handler` — the handler (or client call) that raised, or `None`. * `block` — the related shard block, or `None` for masterchain-level errors. ## Lifecycle The scanner offers three entry points; each runs until `stop()` is called: | Method | Description | | - | - | | `await scanner.start()` | Start from the current last masterchain block. | | `await scanner.resume()` | Continue from the seqno saved in storage. Requires `storage=` and a prior run. | | `await scanner.start_from(...)` | Start from an explicit point: exactly one of `seqno=`, `lt=`, or `utime=`. | | `await scanner.stop()` | Request the scanning loop to stop. | Call `stop()` in a `finally` block so the loop shuts down cleanly, then close the client. ## Full Example ```python theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} import time from pathlib import Path from ton_core import NetworkGlobalID from tonutils.clients import LiteBalancer from tonutils.types import DEFAULT_ADNL_RETRY_POLICY from tonutils.tools.block_scanner import ( BlockEvent, BlockScanner, ErrorEvent, TransactionsEvent, ) # Path to the file where the last processed masterchain seqno is persisted STORAGE_PATH = "block_scanner.mc_seqno" # Maximum requests per second sent to lite-servers RPS_LIMIT = 100 # How far back to start scanning when using start_from(utime=...) START_FROM_OFFSET = 7 * 24 * 60 * 60 # 1 week ago class FileStorage: """File-backed storage for BlockScanner resume support.""" 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") client = LiteBalancer.from_network_config( network=NetworkGlobalID.MAINNET, rps_limit=RPS_LIMIT, retry_policy=DEFAULT_ADNL_RETRY_POLICY, ) # File-backed storage enables scanner.resume() across process restarts storage = FileStorage(STORAGE_PATH) scanner = BlockScanner(client, storage=storage) # ErrorEvent handler must never raise — exceptions inside it are silently dropped @scanner.on_error() async def handle_error(event: ErrorEvent) -> None: where = f"mc_seqno={event.mc_block.seqno}" if event.block is not None: where += f", shard_seqno={event.block.seqno}" print(f"[error] {where}: {type(event.error).__name__}: {event.error}") @scanner.on_block() async def handle_block(event: BlockEvent) -> None: # Emitted once per shard block before the corresponding TransactionsEvent print( f"Block: wc={event.block.workchain}, " f"shard={event.block.shard:016x}, " f"shard_seqno={event.block.seqno}, " f"mc_seqno={event.mc_block.seqno}" ) @scanner.on_transactions() async def handle_transactions(event: TransactionsEvent) -> None: # Emitted after handle_block for the same shard block print( f"Transactions: shard_seqno={event.block.seqno}, " f"mc_seqno={event.mc_block.seqno}, " f"count={len(event.transactions)}" ) async def main() -> None: # Connect to lite-servers before starting the scanner await client.connect() try: # Option 1: Start from the current last masterchain block # await scanner.start() # Option 2: Resume from the last saved seqno in storage # await scanner.resume() # Option 3: Start from an explicit point in time # start_from() accepts exactly one of: seqno=, lt=, or utime= from_utime = int(time.time()) - START_FROM_OFFSET await scanner.start_from(utime=from_utime) finally: # Always stop the scanner and close the client on exit await scanner.stop() await client.close() if __name__ == "__main__": import asyncio from contextlib import suppress with suppress(KeyboardInterrupt): asyncio.run(main()) ``` Full runnable block scanner script on GitHub. # Command-Line Interface Source: https://tonutils.ness.uz/tools/cli Command-line tools bundled with tonutils — version info and live lite-server and DHT status monitors. Installing `tonutils` adds a `tonutils` command with a version flag and live terminal monitors for TON network infrastructure. ## Version ```bash theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} tonutils -v ``` Prints the installed tonutils version. ## Status Monitors ```bash theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} tonutils status ls # monitor lite servers tonutils status dht # monitor DHT nodes ``` `tonutils status` without a subcommand runs the lite-server monitor. `tonutils status ls` polls every lite server from the network config and renders a live table: server software version and time, ping / connect / request round-trip times, latest masterchain and basechain blocks (seqno and transaction count), earliest available archive timestamp, and the most recent error, if any. `tonutils status dht` does the same for DHT nodes: ADNL key ID, address-list version, ping / connect / `dht.findNode` round-trip times, neighbor count, and the most recent error. Both monitors run until interrupted with `Ctrl+C`. ## Options | Option | Commands | Description | | - | - | - | | `-n, --network` | `ls` `dht` | `mainnet` (default) or `testnet` | | `-c, --config` | `ls` `dht` | Config file path or URL | | `-r, --rps` | `ls` | Requests per second (default: `100`) | | `--retry` | `ls` | Enable the default ADNL retry policy | Without `-c`, the public global config for the selected network is used. **Examples:** ```bash theme={"theme":{"light":"github-light-default","dark":"dark-plus"}} # Monitor testnet lite servers tonutils status ls -n testnet # Monitor lite servers from a private config with retries tonutils status ls -c /path/to/config.json --retry # Monitor DHT nodes from a config URL tonutils status dht -c https://ton.org/global-config.json ```