# 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
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)
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)
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
```