ACTON: первый смарт-контракт на TON — практический туториал (2026)
Установка Acton, написание первого Tolk-контракта счётчика, тесты, mutation testing, fuzzing, деплой в testnet — пошаговый практический туториал для TON-разработчика.
- Автор
- TON Adoption Team · исследовательская группа проекта
- Опубликовано
Содержание18разделов
- Что мы построим
- Шаг 1: Установка Acton
- macOS / Linux
- Windows (через WSL)
- Docker (любая ОС)
- Зависимости
- Шаг 2: Создание проекта
- Шаг 3: Код контракта
- Билд
- Шаг 4: Тесты
- Шаг 5: Mutation testing
- Шаг 6: Fuzzing
- Шаг 7: Деплой в testnet
- Шаг 8: Взаимодействие через TonAPI
- Шаг 9: Retrace (для разбора реальных транзакций)
- Что дальше
- Полезные ссылки
- Итого
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 для любого нового контракта:
acton new <name> --template tolk-blank- Написать код контракта
acton build- Написать тесты,
acton test acton test --mutateдо 100% покрытияacton test --fuzzдо отсутствия surprisesacton checkдля статанализа (29 lint-правил)acton script scripts/deploy.tolk --net testnet- После проверки в testnet →
--net mainnet
Каждая ступень займёт меньше времени, чем эквивалент на старом стеке. Тестовый прогон 100 тестов — секунды вместо минут, mutation testing работает на любом контракте, deploy — одна команда.
Полезные ссылки
- Полный гайд по Acton Foundry — обзор всех команд и философии
- Tolk: введение в язык — синтаксис и типы
- TON Foundation grants — гранты для разработчиков
- Acton docs — официальная документация
- Testnet Giver — бесплатный TON для тестов
Итого
Acton — это не просто новый CLI, это качественный скачок в DX TON-разработки. Установка → первый контракт → деплой в testnet за 2 часа, mutation testing и fuzzing из коробки, retrace для разбора реальных транзакций. Если вы впервые пробуете TON — начинайте с Acton, минуя старый стек.
Окно для first-mover-проектов на TON открыто 2-4 месяца после релиза (май 2026 — август 2026). Гранты, internships, hire-направления — всё в активной фазе. Хороший момент войти.
Частые вопросы
Сколько времени уйдёт на туториал, если я знаю TypeScript, но не TON?
Чем Acton принципиально лучше старого Blueprint + Sandbox?
На каких ОС работает Acton?
Нужно ли учить Tolk, или можно делать первые шаги на FunC/Tact?
Что такое sandbox в контексте Acton?
Можно ли через Acton получить грант TON Foundation?
Что такое retrace и зачем он нужен?
Похожие материалы
- Основы14 мая 2026 г.
Acton v1.0 — Foundry для TON: полный гайд (2026)
Acton v1.0 от TON Foundation вышел 11 мая 2026: один Rust-CLI, который заменяет blueprint, sandbox, Misti и func.
- Основы15 дек. 2025 г.
TON для разработчиков: знакомство с FunC, Tact и Tolk
Какие языки используются для смарт-контрактов TON в 2026 — FunC, Tact, Tolk. Что выбрать новичку, чем отличаются, какие инструменты нужны и где учиться.
- Основы14 мая 2026 г.
Tolk: новый язык смарт-контрактов TON в 2026
Tolk — преемник FunC и нативный язык Acton. TypeScript-подобный синтаксис, сильная типизация, pattern matching.
- Новости9 мар. 2026 г.
TON Foundation: кто стоит за развитием блокчейна 2026
Разбор TON Foundation — структура, ключевые люди, история отделения от Telegram, роль Max Crown и Steve Yun, появление TON Strategy Co.
- Основы11 февр. 2026 г.
Mainnet vs Testnet TON: для чего нужны и как переключаться
Чем отличается mainnet от testnet TON, как переключить Tonkeeper в тестовую сеть, где взять бесплатный testnet TON и зачем это нужно разработчикам и.
- Основы21 мая 2026 г.
SDK для TON: tonweb vs ton-core vs tonconnect-sdk — что выбрать в 2026
Сравнение TypeScript-SDK для TON в 2026: tonweb (легаси), @ton/ton + @ton/core (рекомендуемый), @tonconnect/sdk. Когда что выбирать и почему.