Welcome to your journey into TON smart contract development! In this comprehensive tutorial, you’ll learn to build, deploy, and interact with a smart contract from scratch.
A smart contract is a computer program stored on TON Blockchain — distributed database that many computers maintain together. It runs on the TVM (TON Virtual Machine) — the “computer” that runs smart contract code on TON.The contract is made of two parts:
Code (compiled TVM instructions) - the “rules” or “program logic”
Data (persistent state) - the “memory” that remembers things between interactions
Both are stored at a specific address on TON Blockchain, a unique identifier for each smart contract.
Every smart contract in TON is typically divided into three sections: storage, messages, and getters.
Storage: Defines the contract’s persistent data. For example, our counter variable must keep its value across calls from different users.
Messages: Define how the contract reacts to incoming messages. On TON, the primary way to interact with contracts is by sending messages. Each processed message produces a transaction — a recorded change on the blockchain (like “Alice sent 5 TON to Bob”).
Getters: Provide read-only access to contract data without modifying state. For example, we’ll create a getter to return the current value of the counter.
Due to the TON architecture, getters cannot be called from other contracts.
Inter-contract communication is possible only through messages.
First, we need a way to store the counter value. Tolk makes this simple with :
./contracts/first_contract.tolk
struct Storage { counter: uint64; // the current counter value}// load contract data from persistent storagefun Storage.load() { return Storage.fromCell(contract.getData())}// save contract data to persistent storagefun Storage.save(self) { contract.setData(self.toCell())}
Behind the scenes, structures know how to serialize and deserialize themselves into cells — the fundamental way TON stores data. This happens through the fromCell and toCell functions - Tolk automatically converts between your nice structures and the cell format that TON understands.
You may think of cells like containers that hold data on TON:
Each cell can store up to 1023 bits of data.
Cells can reference other cells (like links).
Everything on TON (contracts, messages, storage) is made of cells.
Now that we can store data, let’s handle our first messages.
The main entrypoint for processing messages in a Tolk contract is the onInternalMessage function. It receives one argument — the incoming message. Among its fields, the most important one for us is body, which contains the payload sent by a user or another contract.Tolk structures are also useful for defining message bodies. In our case we’ll define two messages:
IncreaseCounter — with one field increaseBy, used to increment the counter.
ResetCounter — used to reset the counter to zero.
Each structure has a unique prefix (0x7e8764ef and 0x3a752f06), widely called opcodes, that lets the contract distinguish between them.
To group them together, we’ll use a union. Unions allow multiple types to be bundled into a single type that can be serialized and deserialized automatically:
./contracts/first_contract.tolk
type AllowedMessage = IncreaseCounter | ResetCounter;
Now we can write our message handler:
./contracts/first_contract.tolk
fun onInternalMessage(in: InMessage) { // use `lazy` to defer parsing until fields are accessed val msg = lazy AllowedMessage.fromSlice(in.body); // matching our union to determine body structure match (msg) { IncreaseCounter => { // load contract storage lazily (efficient for large or partial reads/updates) var storage = lazy Storage.load(); storage.counter += msg.increaseBy; storage.save(); } ResetCounter => { var storage = lazy Storage.load(); storage.counter = 0; storage.save(); } // this match branch would be executed if message body does not match IncreaseCounter or ResetCounter structures else => { // reject user message (throw) if body is not empty assert(in.body.isEmpty()) throw 0xFFFF } }}
The next step is to build our contract — compile it into bytecode that can be executed by the TVM. With Blueprint, this takes one command:
npx blueprint build FirstContract
Expected output:
Build script running, compiling FirstContract🔧 Using tolk version 1.1.0...✅ Compiled successfully! Cell BOC result:{ "hash": "fbfb4be0cf4ed74123b40d07fb5b7216b0f7d3195131ab21115dda537bad2baf", "hashBase64": "+/tL4M9O10EjtA0H+1tyFrD30xlRMashEV3aU3utK68=", "hex": "b5ee9c7241010401005b000114ff00f4a413f4bcf2c80b0102016202030078d0f891f24020d72c23f43b277c8e1331ed44d001d70b1f01d70b3fa0c8cb3fc9ed54e0d72c21d3a9783431983070c8cb3fc9ed54e0840f01c700f2f40011a195a1da89a1ae167fe3084b2d"}✅ Wrote compilation artifact to build/FirstContract.compiled.json
This compilation artifact contains the contract bytecode and will be used in the deployment step.In the next section, we’ll learn how to deploy this contract to the TON blockchain and interact with it using scripts and wrappers.
Ready to put your contract on-chain? 🚀To deploy, we first need a wrapper class. Wrappers implement the Contract interface and make it easy to interact with contracts from TypeScript.Create a file ./wrappers/FirstContract.ts with the following code:
We depend on @ton/core — a library with base TON types.
The function createFromConfig constructs a wrapper using code (compiled bytecode) and data (the initial storage layout).
The contract address is derived deterministically from code + data using contractAddress. If two contracts have the same code and init data, the calculation of address will result in the same value.
The method sendDeploy sends the first message with stateInit, which triggers deployment. In practice, this can be an empty message with some TON coins attached.
For this tutorial, we’ll use testnet since it’s free and perfect for learning. You can always deploy to mainnet later once you’re confident with your contract.
The sendDeploy method accepts three arguments, but we only pass two because provider.open automatically supplies the ContractProvider as the first argument.Run the script with (learn more about Blueprint deployment):
npx blueprint run deployFirstContract --testnet --tonconnect --tonviewer
Choose your wallet, scan the QR code shown in the console, and approve the transaction in your wallet app.Expected output:
Using file: deployFirstContract? Choose your wallet Tonkeeper<QR_CODE_HERE>Connected to wallet at address: ...Sending transaction. Approve in your wallet...Sent transactionContract deployed at address kQBz-OQQ0Olnd4IPdLGZCqHkpuAO3zdPqAy92y6G-UUpiC_oYou can view it at https://testnet.tonviewer.com/kQBz-OQQ0Olnd4IPdLGZCqHkpuAO3zdPqAy92y6G-UUpiC_o
Follow the link in the console to see your contract on the Tonviewer. Blockchain explorers like Tonviewer allow you to inspect transactions, smart contracts, and account states on the TON blockchain.🎉 Congratulations! Your contract is live on testnet. Let’s interact with it by sending messages and calling get methods.
Technically speaking, we’ve already sent messages to the contract - the deploy message in previous steps. Now let’s see how to send messages with body.First of all, we should update our wrapper class with three methods: sendIncrease, sendReset, and getCounter:
The only difference from the deploy message is that we pass a body to it — remember the cells I talked about previously? The message body is a cell that contains our instructions.
Now that our contract is deployed and we have wrapper methods, let’s interact with it by sending messages.Let’s create a script ./scripts/sendIncrease.ts that would increase the counter:
./scripts/sendIncrease.ts
import { Address, toNano } from '@ton/core';import { FirstContract } from '../wrappers/FirstContract';import { NetworkProvider } from '@ton/blueprint';const contractAddress = Address.parse('<CONTRACT_ADDRESS>');export async function run(provider: NetworkProvider) { const firstContract = provider.open(new FirstContract(contractAddress)); await firstContract.sendIncrease(provider.sender(), { value: toNano('0.05'), increaseBy: 42 }); await provider.waitForLastTransaction();}
Do not forget to replace <CONTRACT_ADDRESS> your actual contract address from Step 5!
Address parsing: Address.parse() converts the string address to a TON Address object
Contract opening: provider.open() creates a connection to the deployed contract
Value attachment: toNano('0.05') converts 0.05 TON to nanotons (the smallest TON unit)
Message parameters: increaseBy: 42 tells the contract to increase the counter by 42
Transaction waiting: waitForLastTransaction() waits for the transaction to be processed on-chain
To run this script:
npx blueprint run sendIncrease --testnet --tonconnect --tonviewer
Expected result:
Using file: sendIncreaseConnected to wallet at address: ...Sending transaction. Approve in your wallet...Sent transactionTransaction 0fc1421b06b01c65963fa76f5d24473effd6d63fc4ea3b6ea7739cc533ba62ee successfully applied!You can view it at https://testnet.tonviewer.com/transaction/fe6380dc2e4fab5c2caf41164d204e2f41bebe7a3ad2cb258803759be41b5734
Wallet Connection: Blueprint connects to your wallet using TON Connect protocol
Transaction Building: The script creates a transaction with the message body containing the opcode 0x7e8764ef and the value 42
User Approval: Your wallet app shows the transaction details for approval
Blockchain Processing: Once approved, the transaction is sent to the TON network
Validator Consensus: Validators need to produce a new block containing your transaction
Contract Execution: The contract receives the message, processes it in the onInternalMessage function, and updates the counter
Confirmation: The transaction hash is returned, and you can view it on the explorer
Let’s create a script ./scripts/sendReset.ts that would reset the counter:
./scripts/sendReset.ts
import { Address, toNano } from '@ton/core';import { FirstContract } from '../wrappers/FirstContract';import { NetworkProvider } from '@ton/blueprint';const contractAddress = Address.parse('<CONTRACT_ADDRESS>');export async function run(provider: NetworkProvider) { const firstContract = provider.open(new FirstContract(contractAddress)); await firstContract.sendReset(provider.sender(), { value: toNano('0.05') }); await provider.waitForLastTransaction();}
To run this script:
npx blueprint run sendReset --testnet --tonconnect --tonviewer
Expected result:
Using file: sendResetConnected to wallet at address: ...Sending transaction. Approve in your wallet...Sent transactionTransaction 0fc1421b06b01c65963fa76f5d24473effd6d63fc4ea3b6ea7739cc533ba62ee successfully applied!You can view it at https://testnet.tonviewer.com/transaction/fe6380dc2e4fab5c2caf41164d204e2f41bebe7a3ad2cb258803759be41b5734
Get methods are special functions in TON smart contracts that allow you to read data without modifying the contract state or spending gas fees. Unlike message-based interactions, get methods:
Cost nothing: No gas fees required since they don’t modify blockchain state
Execute instantly: No need to wait for blockchain confirmation
Read-only: Cannot change contract storage or send messages
To call a get method, use provider.get(<GET_METHOD_NAME>):
./scripts/getCounter.ts
import { Address } from '@ton/core';import { FirstContract } from '../wrappers/FirstContract';import { NetworkProvider } from '@ton/blueprint';const contractAddress = Address.parse('<CONTRACT_ADDRESS>');export async function run(provider: NetworkProvider) { const firstContract = provider.open(new FirstContract(contractAddress)); const counter = await firstContract.getCounter(); console.log('Counter: ', counter);}
Congratulations! You’ve successfully built, deployed, and interacted with your first TON smart contract from scratch. This is a significant achievement in your blockchain development journey!