Язык 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) — он позволяет писать, компилировать и тестировать контракты прямо в браузере.
Tact 1.6: дженерики и расширения структур
В 2025 году Tact 1.6 принёс две большие возможности для повторного использования кода: обобщённые функции и типы (generics) и методы-расширения для структур и контрактов (extends fun). До 1.6 единственным механизмом переиспользования были traits, которые завязаны на контракт; новые средства работают на уровне обычных функций и структур данных.
Generic functions and types
Обобщённые функции позволяют параметризовать поведение типом, не зная конкретного типа на этапе написания. Это компилируется через специализацию: для каждой комбинации типов компилятор генерирует свою копию функции — runtime-стоимости полиморфизма нет.
// Обобщённая функция: применить fn к каждому элементу map
fun map<K, V, U>(input: map<K, V>, fn: V -> U): map<K, U> {
let result: map<K, U> = emptyMap();
foreach (k, v in input) {
result.set(k, fn(v));
}
return result;
}
// Использование с конкретными типами
let balances: map<Address, Int> = ...;
let halved: map<Address, Int> = map<Address, Int, Int>(balances, (v) => v / 2);
Обобщёнными могут быть и struct/message:
struct Pair<A, B> {
first: A;
second: B;
}
fun makePair<A, B>(a: A, b: B): Pair<A, B> {
return Pair<A, B> { first: a, second: b };
}
Compile-time, не runtime
Generics в Tact — это compile-time механизм. Во время компиляции каждый вызов map<Address, Int, Int>(...) подставляет конкретные типы и генерирует свою специализацию. В TVM bytecode никакой “полиморфной диспетчеризации” нет, газовая стоимость такая же, как у вручную написанной функции для конкретных типов.
Struct extensions: extends fun
Методы-расширения позволяют дописать функцию к существующему типу, не модифицируя его определение. Синтаксис — extends fun ИмяМетода(self: Тип, ...):
struct Point {
x: Int;
y: Int;
}
// Метод-расширение: вычисление длины радиус-вектора в квадрате
extends fun normSquared(self: Point): Int {
return self.x * self.x + self.y * self.y;
}
// Метод-расширение, изменяющий self (mutates)
extends mutates fun translate(self: Point, dx: Int, dy: Int) {
self.x += dx;
self.y += dy;
}
// Использование как обычного метода
let p: Point = Point { x: 3, y: 4 };
let n: Int = p.normSquared(); // 25
p.translate(1, 1); // p теперь (4, 5)
Аналогично можно добавлять методы к Message-типам и даже к контрактам — это удобно, когда стандартный trait не подходит, но нужно расширить готовый Jetton/NFT инфраструктурным методом.
Зачем это нужно
- Библиотеки без traits. Утилиты на map/list (фильтрация, агрегация, slice) теперь пишутся как обобщённые функции и переиспользуются в любом контракте.
- Богатые типы данных.
Point.normSquared(),Decimal.add(),BondingCurve.priceFor(supply)— структура несёт не только данные, но и операции над ними, без вырожденных trait-ов с одним полем. - Совместимость со стандартными struct. Можно дописать метод к чужому Message (например, к
JettonTransfer), не форкая stdlib.
Не путайте extends с traits
Trait добавляет функциональность к контракту (поля, receivers, методы). extends fun добавляет метод к типу данных (struct, message, базовый тип). Trait подключается через with, метод-расширение — автоматически по факту наличия в области видимости. Для повторного использования логики в контрактах по-прежнему используйте traits.
Частые ошибки
- Путают struct и message в Tact: message это специальный тип для межконтрактного взаимодействия с обязательным opcode, а struct используется только для внутренних структур данных.
- Забывают объявлять receive-обработчики для bounced-сообщений, что приводит к потере средств при неудачных межконтрактных вызовах.
- Используют String для хранения данных вместо более эффективных типов (Int, Address, Cell), что резко увеличивает расход газа.
- Не понимают систему ownership в Tact: передача Cell или Slice в функцию может «поглотить» значение, делая его недоступным в вызывающем коде.