Skip to main content
This tutorial covers building, deploying, and interacting with a smart contract on TON from start to finish.

Prerequisites

  • Basic programming: variables, functions, if/else statements
  • Basic familiarity with a command‑line interface and executing commands
  • Node.js—v22 or later— download here
    • Check if installed: node -v in terminal
  • Installed TON wallet with Toncoin on testnet

Development environment

1

Set up development environment

Use the Blueprint development toolkit for smart contracts. Start a new project with:
npm create ton@latest -- Example --contractName FirstContract --type tolk-empty
This command creates a project named “Example”, containing a contract named “FirstContract”.The generated project structure is:
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
The TON ecosystem provides editor plugins with syntax support for IDEs and code editors. View them here.
2

Move into the project directory

cd Example

What is a smart contract

A smart contract is a program stored on and executed by the . On-chain, every contract consists of two components:
  • Code — compiled TVM instructions, defines the contract’s logic.
  • Data — persistent state, stores information between interactions.
Both are stored at a specific address on TON blockchain, which is a unique identifier for each smart contract. Smart contracts interact with each other only through messages.

Smart contract layout

A contract’s code consists of three functional sections: storage, messages, and get methods:
  • Storage holds the contract’s persistent state. Example: the counter variable keeps its value across calls from different users.
  • Messages are receivers defined in the contract’s code that specify how the contract should react to each incoming message. Each message triggers a specific action or changes the state according to the contract’s logic.
  • Get methods are read-only functions that return contract data without modifying state. Example: a get method that returns the current counter value.
Due to the TON architecture, get methods cannot be called from other contracts. Inter-contract communication uses messages only.

Write a smart contract

To build a simple counter contract:
  • Start with an initial counter value.
  • Send increase messages to add to the counter or reset messages to set it to 0.
  • Call a get method to return the current counter value.
The contract uses Tolk language.
1

Define contract storage

Open the ./contracts/first_contract.tolk file.To define contract storage, store the counter value. Tolk makes it simple with :
./contracts/first_contract.tolk
struct Storage {
    // the current counter value
    counter: uint64;
}

// 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())
}
Structures serialize and deserialize automatically into cells, the storage unit in TON. The fromCell and toCell functions handle conversion between structures and cells.
2

Implement message handlers

To process messages, implement the onInternalMessage function. It receives one argument — the incoming message. Focus on the body field, which contains the payload sent by a user or another contract.Define two message structures:
  • IncreaseCounter — contains one field increaseBy to increment the counter.
  • ResetCounter — resets the counter to 0.
Each structure has a unique prefix —0x7e8764ef and 0x3a752f06— called opcodes, that which allows the contract to distinguish between messages.
./contracts/first_contract.tolk
struct(0x7e8764ef) IncreaseCounter {
    increaseBy: uint32
}

struct(0x3a752f06) ResetCounter {}
To avoid manual deserialization of each message, group the messages into a union. A union bundles multiple types into a single type and supports automatic serialization and deserialization.
./contracts/first_contract.tolk
type AllowedMessage = IncreaseCounter | ResetCounter;
Now implement the 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 the 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 the 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
        }
    }
}
3

Add getter functions

Write a getter function to return the current counter:
./contracts/first_contract.tolk
get fun currentCounter(): int {
    val storage = lazy Storage.load();
    return storage.counter;
}
4

Complete contract code

The contract now includes:
  • Storage — persistent counter value
  • Messages — IncreaseCounter and ResetCounter handlers
  • Get methods — currentCounter
./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;
}

Compile the contract

To build the contract, compile it into bytecode for execution by the TVM. Use Blueprint with 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
The compilation artifact contains the contract bytecode. This file is required for deployment. Next, deploy the contract to the TON blockchain and interact with it using scripts and wrappers.

Deploy to testnet

1

Create a wrapper file

To deploy, create a wrapper class. Wrappers make it easy to interact with contracts from TypeScript.Open the ./wrappers/FirstContract.ts file and replace its content 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,
        });
    }
}
Wrapper class details:
  • @ton/core — a library with base TON types.
  • The function createFromConfig builds a wrapper using:
    • code — compiled bytecode
    • data — the initial storage layout
  • The contract address is derived deterministically from code and data using contractAddress.
  • The method sendDeploy sends the first message with stateInit, the structure holding the contract’s initial code and data, which triggers deployment. In practice, this can be an empty message with Toncoin attached.
2

Choose the network

TON provides two networks for deployment:
  • testnet — developer sandbox.
  • mainnet — production blockchain.
This tutorial uses testnet. Mainnet deployment is possible once the contract is verified and ready for production.
3

Create the deployment script

Open the ./scripts/deployFirstContract.ts file and replace its content with the following code. It deploys the contract.
./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. Only two arguments are passed because provider.open automatically provides the ContractProvider as the first argument.
4

Run the script

Funds at riskOn-chain deployments spend Toncoin and are irreversible. Verify the network before executing. Use testnet for practice; use mainnet only for actual deployment.
Run the script with:
npx blueprint run deployFirstContract --testnet --tonconnect --tonviewer
For flags and options, see the Blueprint deployment guide.
5

Confirm transaction

Wallet requiredIf a wallet is not installed, check the wallet section to select and install a wallet before deploying the contract. Make sure the wallet is funded with Toncoin on the testnet.
Scan the QR code displayed in the console, and confirm the transaction in the 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
The link opens the contract on Tonviewer, a blockchain explorer showing transactions, messages and account states.Next, interact with the contract by sending messages and calling getter functions.

Contract interaction

Deployment also counts as a message sent to the contract. The next step is to send a message with a body to trigger contract logic.
1

Update wrapper class

Update the 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 main difference from the deploy message is that these methods include a message body. The body is a cell that contains the instructions.Building message bodiesCells are constructed using the beginCell method:
  • beginCell() creates a new cell builder.
  • storeUint(value, bits) appends an unsigned integer with a fixed bit length.
  • endCell() finalizes the cell.
ExamplebeginCell().storeUint(0x7e8764ef, 32).storeUint(42, 32).endCell()
  • First 32 bits: 0x7e8764ef — opcode for “increase”
  • Next 32 bits: 42 — increase by this amount
2

Send messages to the contract

With the contract deployed and wrapper methods in place, the next step is to send messages to it.Create a script ./scripts/sendIncrease.ts that increases the counter:
./scripts/sendIncrease.ts
import { Address, toNano } from '@ton/core';
import { FirstContract } from '../wrappers/FirstContract';
import { NetworkProvider } from '@ton/blueprint';

// `Address.parse()` converts string to address object
const contractAddress = Address.parse('<CONTRACT_ADDRESS>');

export async function run(provider: NetworkProvider) {
  // `provider.open()` creates a connection to the deployed contract
  const firstContract = provider.open(new FirstContract(contractAddress));
  // `toNano('0.05')` converts 0.05 TON to nanotons
  // `increaseBy: 42` tells the contract to increase the counter by 42
  await firstContract.sendIncrease(provider.sender(), { value: toNano('0.05'), increaseBy: 42 });
  // `waitForLastTransaction()` waits for the transaction to be processed on-chain
  await provider.waitForLastTransaction();
}
Replace <CONTRACT_ADDRESS> with the address obtained in the deployment step.
Funds at riskOn-chain deployments spend Toncoin and are irreversible. Verify the network before executing. Use testnet for practice; use mainnet only for actual deployment.
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. Blueprint connects to the wallet using the TON Connect protocol.
  2. The script builds a transaction with a message body containing opcode 0x7e8764ef and value 42.
  3. The wallet displays transaction details for confirmation.
  4. After approval, the transaction is sent to the network.
  5. Validators include the transaction in a newly produced block.
  6. The contract receives the message, processes it in onInternalMessage, and updates the counter.
  7. The script returns the resulting transaction hash; inspect it in the explorer.
ComposabilityOther contracts can also send messages to this contract. This enables composition: different contracts can combine their logic with this one, reuse it as a component, and build new behaviors that were not originally anticipated.
3

Reset the counter

To reset the counter, create a script ./scripts/sendReset.ts:
./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
4

Read contract data with get methods

Get methods are special functions in TON smart contracts that run locally on a node. Unlike message-based interactions, get methods are:
  • Free — no gas fees, as the call does not modify the blockchain state.
  • Instant — no need to wait for blockchain confirmation.
  • Read-only — can only read data; cannot modify storage or send messages.
To call a get method, use getCounter(), which calls the contract’s getter provider.get('currentCounter'):
./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));
    //  `getCounter()` сalls the contract's `currentCounter` getter
    const counter = await firstContract.getCounter(); // returns instantly
    console.log('Counter: ', counter); // wrapper parses stack into JS number
  }
Get methods are available off-chain only — JavaScript clients, web apps, etc. — through RPC providers. Contracts cannot call getters on other contracts — inter-contract interaction uses only messages.
Funds at riskOn-chain deployments spend Toncoin and are irreversible. Verify the network before executing. Use testnet for practice; use mainnet only for actual deployment.
To run this script:
npx blueprint run getCounter --testnet --tonconnect
Expected output:
Using file: getCounter
Counter:  42
The full code for this tutorial is available in the GitHub repository. It includes all contract files, scripts, and wrappers ready to use.