К основному содержанию
T TON Adoption
Основы DEV · 2026

ACTON: первый смарт-контракт на TON — практический туториал (2026)

Установка Acton, написание первого Tolk-контракта счётчика, тесты, mutation testing, fuzzing, деплой в testnet — пошаговый практический туториал для TON-разработчика.

Автор
TON Adoption Team · исследовательская группа проекта
Опубликовано
7 мин. чтения

TL;DR. Acton v1.0 от TON Foundation (вышел 11 мая 2026) консолидирует TON-стек в один CLI: acton new, acton build, acton test, acton deploy. Туториал ниже — пошагово: установка → создание проекта counter → код Tolk → юнит-тесты → mutation testing → деплой в testnet → вызов через TonAPI. От пустой машины до работающего контракта в testnet — 2 часа. Этот документ — практический companion к нашему обзорному гайду по ACTON.

Что мы построим

Простейший контракт-счётчик. Возможности:

  • Хранит число counter в data-cell.
  • Принимает сообщения op::increment (op=0x7e8764ef) и op::reset (op=0xa5a4e3d2).
  • Get-method getCounter() возвращает текущее значение.

Это «hello-world» в TON, как useState в React или tasks в Express. После него у вас будет полный workflow для любого контракта посложнее.

Шаг 1: Установка Acton

macOS / Linux

curl -fsSL https://acton.ton.org/install.sh | sh

Скрипт скачает бинарник в ~/.local/bin/acton. Добавьте в PATH:

echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc  # или ~/.bashrc
source ~/.zshrc
acton --version  # должно показать "acton 1.0.0"

Windows (через WSL)

wsl --install -d Ubuntu-22.04

После установки и перезагрузки откройте Ubuntu и выполните те же шаги, что для Linux.

Docker (любая ОС)

docker pull ghcr.io/ton-blockchain/acton:1.0.0
alias acton='docker run --rm -v $(pwd):/work -w /work ghcr.io/ton-blockchain/acton:1.0.0'

Алиас работает только в текущей сессии терминала; для постоянной установки добавьте в .bashrc/.zshrc.

Зависимости

# Node.js 22 LTS — нужен для frontend-шаблонов и acton func2tolk
nvm install 22
nvm use 22

# Sandbox runtime (опционально, для cross-runtime тестов)
acton extras install sandbox-runtime

Шаг 2: Создание проекта

acton new counter
cd counter

Acton задаст вопросы:

  • Template: tolk-blank (пустой Tolk-контракт) — выбираем
  • Use git: yes
  • Frontend: skip (для туториала не нужен)

Структура проекта:

counter/
├── Acton.toml            # манифест проекта
├── contracts/
│   └── counter.tolk      # код контракта
├── tests/
│   └── counter.test.tolk # тесты на том же Tolk
├── scripts/
│   └── deploy.tolk       # deploy-скрипт
└── README.md

Шаг 3: Код контракта

Откройте contracts/counter.tolk. Замените содержимое на:

import "@stdlib/tvm-dicts"

// Op-codes для сообщений
const OP_INCREMENT: int = 0x7e8764ef;
const OP_RESET: int = 0xa5a4e3d2;

// Storage layout: один cell с одной uint32 переменной
struct Storage {
    counter: uint32,
}

// Загрузка storage из C4
fun loadStorage(): Storage {
    val ds = getData().beginParse();
    val counter = ds.loadUint(32);
    return Storage { counter };
}

// Сохранение storage в C4
fun saveStorage(s: Storage): void {
    setData(
        beginCell()
            .storeUint(s.counter, 32)
            .endCell()
    );
}

// Главная функция: обработка входящих сообщений
fun onInternalMessage(msgValue: int, msgFull: cell, msgBody: slice): void {
    // Empty body? Skip (это обычный TON-transfer)
    if (msgBody.bitsCount() < 32) {
        return;
    }

    val op = msgBody.loadUint(32);
    val storage = loadStorage();

    if (op == OP_INCREMENT) {
        storage.counter += 1;
        saveStorage(storage);
        return;
    }

    if (op == OP_RESET) {
        storage.counter = 0;
        saveStorage(storage);
        return;
    }

    // Unknown op — bounce
    throw 0xffff;
}

// Get-method: возвращает текущее значение counter
@get
fun getCounter(): uint32 {
    return loadStorage().counter;
}

Что здесь происходит:

  • import "@stdlib/tvm-dicts" — подключение стандартной библиотеки (на самом деле в этом примере не используется, но привычка хорошая).
  • struct Storage — модель данных, которая хранится в C4-регистре TVM.
  • loadStorage() / saveStorage() — сериализация/десериализация.
  • onInternalMessage — entry-point при получении сообщения.
  • @get fun getCounter() — read-only метод, доступный извне через TonAPI.

Билд

acton build

Если всё хорошо — увидите:

✓ Compiled contracts/counter.tolk → build/counter.fif
✓ Generated build/counter.code.boc (152 bytes)
✓ Generated build/counter.abi.json

Шаг 4: Тесты

Откройте tests/counter.test.tolk. Замените на:

import "../contracts/counter.tolk"
import "@stdlib/test"

const OP_INCREMENT: int = 0x7e8764ef;
const OP_RESET: int = 0xa5a4e3d2;

@test
fun testInitialState(): void {
    // Deploy with counter = 0
    val contract = TestContract.deploy(
        counter.code,
        beginCell().storeUint(0, 32).endCell(),
    );
    val result = contract.callGet("getCounter");
    assert(result.asInt() == 0, "Initial counter should be 0");
}

@test
fun testIncrement(): void {
    val contract = TestContract.deploy(
        counter.code,
        beginCell().storeUint(0, 32).endCell(),
    );

    // Send increment message
    val msg = beginCell().storeUint(OP_INCREMENT, 32).endCell().beginParse();
    contract.sendInternal(msg, 1_000_000_000); // 1 TON в gas

    val result = contract.callGet("getCounter");
    assert(result.asInt() == 1, "After increment, counter should be 1");
}

@test
fun testMultipleIncrement(): void {
    val contract = TestContract.deploy(
        counter.code,
        beginCell().storeUint(0, 32).endCell(),
    );

    for (i in 0..5) {
        val msg = beginCell().storeUint(OP_INCREMENT, 32).endCell().beginParse();
        contract.sendInternal(msg, 1_000_000_000);
    }

    val result = contract.callGet("getCounter");
    assert(result.asInt() == 5, "After 5 increments, counter should be 5");
}

@test
fun testReset(): void {
    val contract = TestContract.deploy(
        counter.code,
        beginCell().storeUint(42, 32).endCell(), // start at 42
    );

    val msg = beginCell().storeUint(OP_RESET, 32).endCell().beginParse();
    contract.sendInternal(msg, 1_000_000_000);

    val result = contract.callGet("getCounter");
    assert(result.asInt() == 0, "After reset, counter should be 0");
}

@test
fun testUnknownOpBounces(): void {
    val contract = TestContract.deploy(
        counter.code,
        beginCell().storeUint(0, 32).endCell(),
    );

    val msg = beginCell().storeUint(0xdeadbeef, 32).endCell().beginParse();
    val txResult = contract.sendInternal(msg, 1_000_000_000);

    assert(txResult.exitCode == 0xffff, "Unknown op should throw 0xffff");
}

Запустить:

acton test

Должно показать:

✓ testInitialState (1ms)
✓ testIncrement (2ms)
✓ testMultipleIncrement (5ms)
✓ testReset (2ms)
✓ testUnknownOpBounces (2ms)

5 tests passed, 0 failed in 12ms

Шаг 5: Mutation testing

Mutation testing проверяет, насколько хорошо ваши тесты покрывают код. Acton автоматически модифицирует контракт-код (например, меняет += 1 на -= 1, == на !=, удаляет throw) и запускает все тесты — если какой-то тест не сломался при модификации, значит покрытие недостаточное.

acton test --mutate

Вывод:

Running mutation tests on contracts/counter.tolk...

Mutation 1: line 33 `storage.counter += 1` → `storage.counter -= 1`
  ✓ Caught by testIncrement (counter became -1, expected 1)

Mutation 2: line 38 `storage.counter = 0` → `storage.counter = 1`
  ✓ Caught by testReset (counter became 1, expected 0)

Mutation 3: line 45 `throw 0xffff` → removed
  ✓ Caught by testUnknownOpBounces (no throw, exitCode=0)

Mutation 4: line 28 `if (msgBody.bitsCount() < 32)` → `if (msgBody.bitsCount() > 32)`
  ⚠ Survived — no test covers empty-body case

3/4 mutations caught (75% coverage). Add tests for surviving mutations.

Survived mutation = пробел в тестах. Добавим:

@test
fun testEmptyBodyIgnored(): void {
    val contract = TestContract.deploy(
        counter.code,
        beginCell().storeUint(5, 32).endCell(),
    );

    // Empty body — should not change counter
    val emptyMsg = beginCell().endCell().beginParse();
    contract.sendInternal(emptyMsg, 1_000_000_000);

    val result = contract.callGet("getCounter");
    assert(result.asInt() == 5, "Empty body should not modify counter");
}

Перезапустить acton test --mutate → теперь 4/4 (100%) coverage.

Шаг 6: Fuzzing

Fuzzing — случайные входные данные для поиска edge-cases:

acton test --fuzz

Acton сгенерирует 1000+ случайных входов:

  • Random op-codes (включая невалидные)
  • Random msgValue (от 0 до huge числа)
  • Empty/malformed slices
  • Maximum-size cells

Если контракт где-то выбрасывает не пойманную ошибку или потребляет неожиданно много газа — fuzzing найдёт.

Шаг 7: Деплой в testnet

Сначала нужен кошелёк для testnet с балансом. Получите через Testnet TON Bot — это бесплатно.

# Сохраните mnemonic в .env (НЕ коммитьте!)
echo "DEPLOYER_MNEMONIC='word1 word2 ... word24'" >> .env

# Деплой
acton script scripts/deploy.tolk --net testnet

scripts/deploy.tolk:

import "../contracts/counter.tolk"

@script
fun deploy(): void {
    val deployer = Wallet.fromMnemonic(env("DEPLOYER_MNEMONIC"));
    val initialData = beginCell().storeUint(0, 32).endCell();

    val deployed = deployer.deployContract(
        counter.code,
        initialData,
        1_000_000_000, // 1 TON на gas
    );

    print("Contract deployed at:");
    print(deployed.address.toString());
}

После успешного деплоя — адрес контракта в логах. Откройте на testnet.tonviewer.com:

EQA7ml...

Шаг 8: Взаимодействие через TonAPI

Из TypeScript/Node, например:

import { TonApiClient } from '@ton-api/client';
import { TonClient, Address, beginCell, internal, WalletContractV4 } from '@ton/ton';
import { mnemonicToWalletKey } from '@ton/crypto';

const CONTRACT_ADDRESS = 'EQA7ml...'; // из деплоя

async function callIncrement() {
  const tonapi = new TonApiClient({ apiKey: process.env.TONAPI_KEY });
  const tonClient = new TonClient({
    endpoint: 'https://testnet.toncenter.com/api/v2/jsonRPC',
  });

  const key = await mnemonicToWalletKey(process.env.DEPLOYER_MNEMONIC!.split(' '));
  const wallet = WalletContractV4.create({
    workchain: 0,
    publicKey: key.publicKey,
  });
  const contract = tonClient.open(wallet);

  // Op code 0x7e8764ef = increment
  const body = beginCell().storeUint(0x7e8764ef, 32).endCell();

  await contract.sendTransfer({
    seqno: await contract.getSeqno(),
    secretKey: key.secretKey,
    messages: [
      internal({
        to: Address.parse(CONTRACT_ADDRESS),
        value: '0.05',
        body,
      }),
    ],
  });

  console.log('Increment sent. Waiting for confirmation...');
  // Через 30 секунд проверьте состояние:
  await new Promise(r => setTimeout(r, 30_000));

  const counter = await tonapi.blockchain.execGetMethodForBlockchainAccount(
    CONTRACT_ADDRESS,
    'getCounter',
  );
  console.log('New counter value:', counter.decoded);
}

callIncrement().catch(console.error);

После выполнения — на tonviewer.com увидите две транзакции (от кошелька → к контракту), и counter обновился на 1.

Шаг 9: Retrace (для разбора реальных транзакций)

Если хотите изучить, как работает контракт-эксплойт или просто разобрать транзакцию в детали:

acton retrace 7ef9b0a3... --net testnet --debug

Откроется debugger-режим:

  • Step-into каждой инструкции TVM
  • Просмотр стека, регистров, газа на каждом шаге
  • Breakpoints на конкретные op-коды

Это game-changer для security-аудитов — раньше такого инструмента на TON не было.

Что дальше

Готовый workflow для любого нового контракта:

  1. acton new <name> --template tolk-blank
  2. Написать код контракта
  3. acton build
  4. Написать тесты, acton test
  5. acton test --mutate до 100% покрытия
  6. acton test --fuzz до отсутствия surprises
  7. acton check для статанализа (29 lint-правил)
  8. acton script scripts/deploy.tolk --net testnet
  9. После проверки в testnet → --net mainnet

Каждая ступень займёт меньше времени, чем эквивалент на старом стеке. Тестовый прогон 100 тестов — секунды вместо минут, mutation testing работает на любом контракте, deploy — одна команда.

Полезные ссылки

Итого

Acton — это не просто новый CLI, это качественный скачок в DX TON-разработки. Установка → первый контракт → деплой в testnet за 2 часа, mutation testing и fuzzing из коробки, retrace для разбора реальных транзакций. Если вы впервые пробуете TON — начинайте с Acton, минуя старый стек.

Окно для first-mover-проектов на TON открыто 2-4 месяца после релиза (май 2026 — август 2026). Гранты, internships, hire-направления — всё в активной фазе. Хороший момент войти.

Частые вопросы

Если у вас уже есть опыт с программированием и hot-reload-разработкой — установка Acton + первый контракт + тесты занимает 1.5-2 часа. Изучение Tolk-синтаксиса (он близок к Rust + С) — ещё 2-3 часа на первый serious-контракт. Mutation testing и retrace — это уже на третий-четвёртый контракт, не для первого. Полная адаптация (умение писать продакшен-контракт с нуля) — около 40-60 часов практики.
Скорость: Acton-тесты исполняются Rust-эмулятором TVM, который на порядок быстрее JS-эмулятора @ton/sandbox. Один большой проект (10k тестов) на Blueprint занимал 90 секунд, на Acton — 4 секунды. Интеграция: mutation testing, fuzzing, debugger, formatter, retrace mainnet-транзакций — всё одной командой. Качество: каждая команда покрыта типами и тестами, регрессий меньше.
macOS (ARM64 + x86_64), Linux GNU x86_64 — нативно. Windows — только через WSL (Ubuntu 20.04+). Native Windows-бинарник на середину 2026 не планируется (Rust-toolchain слишком зависит от POSIX). Docker-образ ghcr.io/ton-blockchain/acton:1.0.0 работает на любой ОС с docker, включая Windows + Docker Desktop.
Acton поддерживает FunC (компилируется через старый бэкенд), но новые проекты идут на Tolk. Tact с Acton работает частично — не все возможности (mutation testing, fuzzing) полностью совместимы с Tact-проектами. Если первый раз — берите Tolk: язык очень похож на Rust, синтаксис чистый, ошибки понятные. Existing FunC-кодовая база может быть конвертирована командой acton func2tolk автоматически на 80-90%.
Sandbox — это Rust-based эмулятор TVM (TON Virtual Machine), который запускает ваш контракт в изоляции и позволяет тестировать без реального блокчейна. Через sandbox вы можете: отправить сообщение контракту, проверить, что get-method возвращает ожидаемое значение, измерить расход газа, посмотреть, какие транзакции произошли. В Blueprint было @ton/sandbox (JS), в Acton — встроенный нативный sandbox.
Да. После релиза Acton v1.0 в мае 2026 TON Foundation особенно охотно даёт гранты на: Acton-плагины, IDE-расширения (VS Code / IntelliJ), обучающие проекты (туториалы, видео), индексаторы поверх ton-indexer крейта, deployment-shortcuts (one-click deploy для популярных шаблонов). Подача через ton-society/grants-and-bounties на GitHub. Размер первого гранта обычно 5-15K USD.
Retrace — это команда `acton retrace <TX_HASH>`, которая берёт реальную транзакцию из mainnet/testnet и реконструирует её локально с debugger'ом, breakpoints, step-into. Главное применение: разбор подозрительных транзакций (хаков, эксплойтов), форензика по incident'ам. До Acton такой возможности на TON не было; разработчики поддерживали свой стек скриптов. Retrace — game-changer для security-аудита и обучения.

Похожие материалы