Язык Tact
Tact — это высокоуровневый язык для разработки смарт-контрактов на TON, спроектированный специально для модели акторов. Если FunC — это «ассемблер» TON, то Tact — это его «TypeScript»: безопасный, выразительный и с удобным синтаксисом. Выбор Tact как основного языка значительно ускоряет разработку и снижает вероятность ошибок, характерных для низкоуровневого программирования на FunC.
Tact — высокоуровневый язык для разработки смарт-контрактов на TON. Он компилируется в FunC, затем в TVM bytecode. Tact предоставляет строгую типизацию, удобный синтаксис и встроенные паттерны безопасности.
Почему Tact?
- Высокоуровневый — абстрагирует работу с ячейками (cells) и слайсами (slices)
- Типобезопасный — ошибки типов ловятся на этапе компиляции
- Встроенные паттерны — traits (Deployable, Ownable) для типовых задач
- Автоматическая сериализация — Tact сам упаковывает данные в cells
Tact (.tact) → FunC (.fc) → TVM bytecode → Блокчейн
Структура контракта
Контракт в Tact — это блок contract, содержащий переменные состояния, инициализатор, receivers и функции:
contract Counter with Deployable {
// Переменные состояния
counter: Int as uint32;
owner: Address;
// Инициализатор (вызывается при деплое)
init(owner: Address) {
self.counter = 0;
self.owner = owner;
}
// Receiver -- обработчик сообщения
receive("increment") {
self.counter += 1;
}
// Get-метод -- чтение данных (off-chain)
get fun counter(): Int {
return self.counter;
}
}
Разберём каждую часть подробнее.
Переменные состояния
Состояние контракта объявляется как поля с типами. Эти данные хранятся в блокчейне (в дереве ячеек контракта).
contract Storage {
count: Int as uint32; // Целое число (32 бита)
active: Bool; // Булево значение
owner: Address; // Адрес кошелька/контракта
data: Cell; // Произвольная ячейка данных
names: map<Int, Address>; // Словарь (ключ → значение)
init() {
self.count = 0;
self.active = true;
self.owner = sender();
self.data = emptyCell();
// map инициализируется пустым автоматически
}
}
Поддерживаемые типы
| Тип | Описание | Пример |
|---|---|---|
Int | Целое число (257 бит по умолчанию) | as uint32, as int8 |
Bool | Булево значение | true, false |
Address | Адрес в сети TON | sender(), myAddress() |
Cell | Произвольная ячейка | emptyCell() |
Slice | Срез ячейки для чтения | cell.asSlice() |
String | Строка | "hello" |
map<K, V> | Словарь | map<Int, Address> |
Используйте модификатор as для экономии газа: Int as uint32 занимает 32 бита вместо 257. Выбирайте минимальный размер, достаточный для ваших данных.
Traits — повторно используемые паттерны
Traits — это наборы функциональности, которые контракт может подключить через with:
import "@stdlib/deploy";
import "@stdlib/ownable";
contract MyContract with Deployable, Ownable {
owner: Address;
init(owner: Address) {
self.owner = owner;
}
}
Основные стандартные traits:
| Trait | Что добавляет |
|---|---|
Deployable | Обработку деплоя (receiver для Deploy-сообщения) |
Ownable | Поле owner, проверку requireOwner() |
OwnableTransferable | Ownable + передача владения |
Stoppable | Возможность “остановить” контракт |
Resumable | Stoppable + возможность возобновления |
Receivers — обработчики сообщений
Receivers — центральная концепция Tact. Это функции, которые вызываются при получении сообщений.
Текстовые receivers
Обрабатывают сообщения с текстовым телом:
contract TextReceiver {
value: Int as uint32;
init() {
self.value = 0;
}
receive("increment") {
self.value += 1;
}
receive("reset") {
self.value = 0;
}
}
Типизированные receivers
Обрабатывают сообщения определённой структуры:
// Определение структуры сообщения
message Add {
amount: Int as uint32;
}
message Subtract {
amount: Int as uint32;
}
contract Calculator {
result: Int as int64;
init() {
self.result = 0;
}
receive(msg: Add) {
self.result += msg.amount;
}
receive(msg: Subtract) {
self.result -= msg.amount;
}
get fun result(): Int {
return self.result;
}
}
Специальные receivers
contract SpecialReceivers {
init() {}
// Получение пустого сообщения (только TON)
receive() {
// Просто принять TON
}
// Обработка bounced-сообщений
bounced(msg: bounced<Add>) {
// Откатить состояние, если сообщение отскочило
}
// Обработка внешних сообщений
external("extMessage") {
// Обработка внешнего сообщения
}
}
Функции
Tact поддерживает несколько типов функций:
contract Functions {
value: Int as uint32;
init() {
self.value = 0;
}
// Обычная функция (внутренняя, для повторного использования)
fun add(a: Int, b: Int): Int {
return a + b;
}
// Get-метод (вызывается off-chain)
get fun value(): Int {
return self.value;
}
// Функция с модификатором mutates (может менять self)
// Все receiver-функции неявно mutates
receive("double") {
self.value = self.add(self.value, self.value);
}
}
| Тип функции | Синтаксис | Назначение |
|---|---|---|
| Обычная | fun name() | Внутренняя логика контракта |
| Get-метод | get fun name() | Чтение данных off-chain |
| Inline | inline fun name() | Встраивание кода (оптимизация газа) |
Управление потоком
Tact поддерживает стандартные конструкции:
// if / else
if (self.value > 100) {
self.value = 100;
} else {
self.value += 1;
}
// while
let i: Int = 0;
while (i < 10) {
i += 1;
}
// repeat -- выполнить N раз
repeat (5) {
self.value += 1;
}
// try / catch
try {
// код, который может бросить исключение
nativeThrowUnless(100, self.value > 0);
} catch (e) {
// обработка ошибки
}
Пример: контракт SimpleStorage
Соберём все концепции в одном контракте:
import "@stdlib/deploy";
import "@stdlib/ownable";
message UpdateValue {
newValue: Int as uint32;
}
contract SimpleStorage with Deployable, Ownable {
owner: Address;
value: Int as uint32;
lastUpdated: Int as uint64;
init(owner: Address) {
self.owner = owner;
self.value = 0;
self.lastUpdated = 0;
}
receive(msg: UpdateValue) {
self.requireOwner(); // Только владелец может обновлять
self.value = msg.newValue;
self.lastUpdated = now(); // Текущий timestamp
}
receive("reset") {
self.requireOwner();
self.value = 0;
}
get fun value(): Int {
return self.value;
}
get fun lastUpdated(): Int {
return self.lastUpdated;
}
get fun owner(): Address {
return self.owner;
}
}
Для быстрого экспериментирования с Tact используйте Tact Playground (https://tact-playground.com) — он позволяет писать, компилировать и тестировать контракты прямо в браузере.
Частые ошибки
- Путают struct и message в Tact: message это специальный тип для межконтрактного взаимодействия с обязательным opcode, а struct используется только для внутренних структур данных.
- Забывают объявлять receive-обработчики для bounced-сообщений, что приводит к потере средств при неудачных межконтрактных вызовах.
- Используют String для хранения данных вместо более эффективных типов (Int, Address, Cell), что резко увеличивает расход газа.
- Не понимают систему ownership в Tact: передача Cell или Slice в функцию может «поглотить» значение, делая его недоступным в вызывающем коде.
Проверка знанийЧем текстовый receiver (receive('increment')) отличается от типизированного (receive(msg: Add))?
Check Your Understanding
Finished the lesson?
Mark it as complete to track your progress
Войдите чтобы оценить урок