Skip to main content

Your first smart contract

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.

What you’ll learn

By the end of this tutorial, you’ll have:
  • Built a complete smart contract in Tolk
  • Deployed it to TON testnet
  • Interacted with it using TypeScript scripts
  • Mastered the fundamentals of TON development

What is a TON smart contract?

Understanding the basics

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.

Prerequisites

  • Basic programming - Understanding of variables, functions, if/else statements
  • Command line basics - Comfortable opening terminal and running commands
  • Node.js (v22 or later) — Download here
  • Check if installed: node -v in terminal
  • TON wallet - We’ll set this up together in Step 5 (TODO: NOT DONE YET, WAIT FOR FAUCET COMPONENT)

Tutorial overview

This tutorial is organized into 6 clear steps that build upon each other:
StepWhat You’ll DoKey Skills
Step 1Set up Blueprint toolkitProject structure, development environment
Step 2Learn contract architectureStorage, messages, getters concept
Step 3Write contract in TolkProgramming, message handling, data structures
Step 4Compile to bytecodeBuild process, TVM compilation
Step 5Deploy to blockchainTestnet deployment, wallet integration
Step 6Interact with contractMessage sending, get methods, TypeScript integration
Let’s dive into development!

Step 1: Development environment setup

We’ll use Blueprint as our development toolkit for smart contracts. Start a new project with:
npm create ton@latest -- Example --contractName FirstContract --type tolk-empty
This will create a project Example with a contract FirstContract. The project structure will look like this:
Example/
├── contracts/                       # Smart contract source code
│   └── first_contract.tolk          # Main contract file
├── scripts/                         # Deployment and on-chain interaction scripts
│   └── deployFirstContract.ts       # Script to deploy the contract
├── tests/                           # Testing specifications
│   └── FirstContract.spec.ts        # Contract test file
└── wrappers/                        # TypeScript wrappers for contract interaction
    ├── FirstContract.ts             # Wrapper class for the smart contract
    └── FirstContract.compile.ts     # Configuration for compiling contract
TON provides plugins that add syntax support for various IDEs and code editors. Check out the IDE & plugins section for VS Code and JetBrains support.
Now, move into the project directory:
cd Example

Step 2: Understanding smart contract architecture

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.

Step 3: Writing the smart contract

We’ll build a simple counter contract:
  • The counter starts from an initial number.
  • Users can send an increase message to increment it, or a reset message to drop it to zero.
  • A getter function will let anyone query the current counter value.
We’ll use Tolk to implement this. Tolk looks familiar if you know TypeScript or Rust, but it’s designed specifically for smart contract development.

3.1 Defining contract storage

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 storage
fun Storage.load() {
    return Storage.fromCell(contract.getData())
}

// save contract data to persistent storage
fun 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.

3.2 Implementing message handlers

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.
./contracts/first_contract.tolk
struct(0x7e8764ef) IncreaseCounter {
    increaseBy: uint32
}

struct(0x3a752f06) ResetCounter {}
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
        }
    }
}
📚 Learn More About Tolk

3.3 Adding getter functions

Finally, let’s implement a getter so users can read the current counter value:
./contracts/first_contract.tolk
get fun currentCounter(): int {
    val storage = lazy Storage.load();
    return storage.counter;
}

3.4 Complete contract code

We now have a complete smart contract with:
  • Storage: persistent counter value
  • Messages: IncreaseCounter and ResetCounter handlers
  • Getter: currentCounter
Here’s the full source code of contracts/first_contract.tolk:
./contracts/first_contract.tolk
struct Storage {
    counter: uint64;
}

fun Storage.load() {
    return Storage.fromCell(contract.getData());
}

fun Storage.save(self) {
    contract.setData(self.toCell());
}

struct(0x7e8764ef) IncreaseCounter {
    increaseBy: uint32
}

struct(0x3a752f06) ResetCounter {}

type AllowedMessage = IncreaseCounter | ResetCounter;

fun onInternalMessage(in: InMessage) {
    val msg = lazy AllowedMessage.fromSlice(in.body);

    match (msg) {
        IncreaseCounter => {
            var storage = lazy Storage.load();
            storage.counter += msg.increaseBy;
            storage.save();
        }

        ResetCounter => {
            var storage = lazy Storage.load();
            storage.counter = 0;
            storage.save();
        }

        else => {
            assert(in.body.isEmpty()) throw 0xFFFF;
        }
    }
}

get fun currentCounter(): int {
    val storage = lazy Storage.load();
    return storage.counter;
}
🎉 Congratulations — you’ve built your first smart contract in Tolk!
📁 Complete Example CodeYou can find the full working code for this tutorial in our GitHub repository. This includes all contract files, scripts, and wrappers ready to use.

Step 4: Compiling your contract

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.

Step 5: Deploying to testnet

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:
./wrappers/FirstContract.ts
import { Address, beginCell, Cell, Contract, contractAddress, ContractProvider, Sender, SendMode } from '@ton/core';

export class FirstContract implements Contract {
    constructor(
        readonly address: Address,
        readonly init?: { code: Cell; data: Cell },
    ) {}

    static createFromConfig(config: { counter: number }, code: Cell, workchain = 0) {
        const data = beginCell().storeUint(config.counter, 64).endCell();
        const init = { code, data };
        return new FirstContract(contractAddress(workchain, init), init);
    }

    async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) {
        await provider.internal(via, {
            value,
            sendMode: SendMode.PAY_GAS_SEPARATELY,
        });
    }
}

5.1 Understanding the wrapper class

  • 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.

5.2 Choosing your network

TON has two networks available for deployment:
  • testnet — developer sandbox.
  • mainnet — production blockchain.
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.

5.3 Creating the deployment script

Blueprint makes deployment simple. Create a new script ./scripts/deployFirstContract.ts:
./scripts/deployFirstContract.ts
import { toNano } from '@ton/core';
import { FirstContract } from '../wrappers/FirstContract';
import { compile, NetworkProvider } from '@ton/blueprint';

export async function run(provider: NetworkProvider) {
    const firstContract = provider.open(
        FirstContract.createFromConfig(
            { counter: Math.floor(Math.random() * 10000000) },
            await compile('FirstContract')
        )
    );

    await firstContract.sendDeploy(provider.sender(), toNano('0.05'));

    await provider.waitForDeploy(firstContract.address);
}
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 transaction
Contract deployed at address kQBz-OQQ0Olnd4IPdLGZCqHkpuAO3zdPqAy92y6G-UUpiC_o
You 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.

Step 6: Contract interaction

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:
./wrappers/FirstContract.ts
import { Address, beginCell, Cell, Contract, contractAddress, ContractProvider, Sender, SendMode } from '@ton/core';

export class FirstContract implements Contract {
    constructor(
        readonly address: Address,
        readonly init?: { code: Cell; data: Cell },
    ) {}

    static createFromConfig(config: { counter: number }, code: Cell, workchain = 0) {
        const data = beginCell().storeUint(config.counter, 64).endCell();
        const init = { code, data };
        return new FirstContract(contractAddress(workchain, init), init);
    }

    async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) {
        await provider.internal(via, {
            value,
            sendMode: SendMode.PAY_GAS_SEPARATELY,
            body: beginCell().endCell(),
        });
    }

    async sendIncrease(
        provider: ContractProvider,
        via: Sender,
        opts: {
            increaseBy: number;
            value: bigint;
        },
    ) {
        await provider.internal(via, {
            value: opts.value,
            sendMode: SendMode.PAY_GAS_SEPARATELY,
            body: beginCell().storeUint(0x7e8764ef, 32).storeUint(opts.increaseBy, 32).endCell(),
        });
    }

    async sendReset(
        provider: ContractProvider,
        via: Sender,
        opts: {
            value: bigint;
        },
    ) {
        await provider.internal(via, {
            value: opts.value,
            sendMode: SendMode.PAY_GAS_SEPARATELY,
            body: beginCell().storeUint(0x3a752f06, 32).endCell(),
        });
    }

    async getCounter(provider: ContractProvider) {
        const result = await provider.get('currentCounter', []);
        return result.stack.readNumber();
    }
}
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.

Building message bodies

Construction of cells starts with the beginCell method (learn more about cell serialization):
  • beginCell() - creates a new cell builder
  • storeUint(value, bits) - adds an unsigned integer of specified bit length
  • endCell() - finalizes the cell
Example: beginCell().storeUint(0x7e8764ef, 32).storeUint(42, 32).endCell()
  • First 32 bits: 0x7e8764ef (opcode for “increase”)
  • Next 32 bits: 42 (increase by this amount)

6.1 Sending messages to your contract

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!

Understanding the script breakdown:

  • 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: sendIncrease
Connected to wallet at address: ...
Sending transaction. Approve in your wallet...
Sent transaction
Transaction 0fc1421b06b01c65963fa76f5d24473effd6d63fc4ea3b6ea7739cc533ba62ee successfully applied!
You can view it at https://testnet.tonviewer.com/transaction/fe6380dc2e4fab5c2caf41164d204e2f41bebe7a3ad2cb258803759be41b5734

What happens during execution:

  1. Wallet Connection: Blueprint connects to your wallet using TON Connect protocol
  2. Transaction Building: The script creates a transaction with the message body containing the opcode 0x7e8764ef and the value 42
  3. User Approval: Your wallet app shows the transaction details for approval
  4. Blockchain Processing: Once approved, the transaction is sent to the TON network
  5. Validator Consensus: Validators need to produce a new block containing your transaction
  6. Contract Execution: The contract receives the message, processes it in the onInternalMessage function, and updates the counter
  7. Confirmation: The transaction hash is returned, and you can view it on the explorer
Composability: Messages can be sent to your contract by other contracts too! This means different contracts can increment your counter, allowing the TON ecosystem to create composable apps and protocols that build on top of each other and interact in unforeseen ways.
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: sendReset
Connected to wallet at address: ...
Sending transaction. Approve in your wallet...
Sent transaction
Transaction 0fc1421b06b01c65963fa76f5d24473effd6d63fc4ea3b6ea7739cc533ba62ee successfully applied!
You can view it at https://testnet.tonviewer.com/transaction/fe6380dc2e4fab5c2caf41164d204e2f41bebe7a3ad2cb258803759be41b5734

6.2 Reading contract data with get methods

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);
}

Understanding the get method execution:

  1. Direct contract call: The getCounter() method directly calls the contract’s currentCounter getter
  2. Instant response: The result is returned immediately without blockchain confirmation
  3. Data parsing: Our wrapper automatically converts the returned stack value to a JavaScript number
Getters are only accessible off-chain (from JavaScript clients, web apps, etc.) through RPC service providers. Contracts cannot call getters on other contracts - inter-contract communication must use messages only.
To run this script:
npx blueprint run getCounter --testnet --tonconnect
Expected output:
Using file: getCounter
Counter:  42

🎉 Tutorial complete!

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!

Continue Your Learning Journey

Ready to build more advanced contracts? Here’s your roadmap:

Next Steps

Advanced Topics

Tutorial CreditsThis tutorial was enhanced with insights from the excellent TON Hello World series by Tal Kol and the TON community. For additional examples and alternative approaches, check out the original series.
🎉 Happy coding on TON! You’re now equipped with the fundamentals to build amazing smart contracts. The blockchain world awaits your innovations! 🚀
I