Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 04.02 · 25 мин
Средний
TactSmart ContractsTypesReceiversTraits

Язык 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Адрес в сети TONsender(), myAddress()
CellПроизвольная ячейкаemptyCell()
SliceСрез ячейки для чтенияcell.asSlice()
StringСтрока"hello"
map<K, V>Словарьmap<Int, Address>
TIP

Используйте модификатор 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()
OwnableTransferableOwnable + передача владения
StoppableВозможность “остановить” контракт
ResumableStoppable + возможность возобновления

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
Inlineinline 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;
    }
}
TIP

Для быстрого экспериментирования с 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 };
}
NOTE

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.
WARNING

Не путайте extends с traits

Trait добавляет функциональность к контракту (поля, receivers, методы). extends fun добавляет метод к типу данных (struct, message, базовый тип). Trait подключается через with, метод-расширение — автоматически по факту наличия в области видимости. Для повторного использования логики в контрактах по-прежнему используйте traits.


Частые ошибки

  1. Путают struct и message в Tact: message это специальный тип для межконтрактного взаимодействия с обязательным opcode, а struct используется только для внутренних структур данных.
  2. Забывают объявлять receive-обработчики для bounced-сообщений, что приводит к потере средств при неудачных межконтрактных вызовах.
  3. Используют String для хранения данных вместо более эффективных типов (Int, Address, Cell), что резко увеличивает расход газа.
  4. Не понимают систему ownership в Tact: передача Cell или Slice в функцию может «поглотить» значение, делая его недоступным в вызывающем коде.

Проверка знанийKnowledge check
Чем текстовый receiver (receive('increment')) отличается от типизированного (receive(msg: Add))?
ОтветAnswer
Текстовый receiver обрабатывает сообщения с простым текстовым телом (строкой), например 'increment'. Типизированный receiver обрабатывает сообщения определённой структуры (message struct) с полями данных. Типизированные receivers позволяют передавать параметры (например, msg.amount), тогда как текстовые подходят для простых команд без параметров.

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 10