Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 04.05 · 25 мин
Средний
MessagesBouncingSend ModesError HandlingInternal Messages

Сообщения и Bouncing

Механизм bouncing — это система «возврата писем» в TON: если контракт не может обработать входящее сообщение, оно возвращается отправителю. Это фундаментальный механизм безопасности, защищающий от потери средств при ошибках. Без правильной обработки bounced-сообщений ваш контракт может записать «деньги отправлены», хотя получатель их не принял, что приведёт к потере средств пользователей.

Сообщения — основной механизм взаимодействия между контрактами в TON. В этом уроке мы разберём структуру сообщений, отправку, режимы и критически важный механизм bouncing (отскока).


Bounced-сообщения: первый пример

Начнём с главного — bounced-сообщения (отскочившие сообщения). Это механизм безопасности TON: если контракт не смог обработать входящее сообщение, оно возвращается отправителю.

message Transfer {
    amount: Int as coins;
    to: Address;
}

contract SafeWallet with Deployable {
    balance: Int as coins;
    owner: Address;
    pendingTransfer: Int as coins;

    init(owner: Address) {
        self.owner = owner;
        self.balance = 0;
        self.pendingTransfer = 0;
    }

    // Отправка перевода
    receive(msg: Transfer) {
        require(sender() == self.owner, "Only owner");
        require(self.balance >= msg.amount, "Insufficient balance");

        // Запоминаем сумму на случай bounce
        self.pendingTransfer = msg.amount;
        self.balance -= msg.amount;

        // Отправляем сообщение получателю
        send(SendParameters{
            to: msg.to,
            value: 0,
            mode: SendRemainingValue,
            bounce: true,           // ВАЖНО: включаем bounce
            body: Transfer{
                amount: msg.amount,
                to: msg.to,
            }.toCell(),
        });
    }

    // Обработка bounce -- КРИТИЧЕСКИ ВАЖНО
    bounced(msg: bounced<Transfer>) {
        // Сообщение вернулось -- получатель не смог принять
        // Восстанавливаем баланс
        self.balance += self.pendingTransfer;
        self.pendingTransfer = 0;
    }

    receive("deposit") {
        self.balance += context().value;
    }

    get fun balance(): Int {
        return self.balance;
    }
}
WARNING

Не обрабатывать bounced-сообщения — опасно!

Если контракт отправляет TON другому контракту, а тот не может принять сообщение, средства “отскочат” назад. Без bounced receiver эти средства будут потеряны — контракт получит bounce-сообщение, но не сможет его обработать, и баланс не восстановится.


Типы сообщений

TON различает два основных типа сообщений:

Internal messages (внутренние)

Сообщения между контрактами внутри блокчейна:

  • Отправляются контрактами и кошельками
  • Несут TON для оплаты газа
  • Обрабатываются автоматически валидаторами

External messages (внешние)

Сообщения из внешнего мира в блокчейн:

  • Отправляются с устройства пользователя
  • Не содержат TON (контракт платит за газ из своего баланса)
  • Первичная точка входа для пользовательских действий

Структура сообщений в Tact

Сообщения определяются как message structs:

// Простое сообщение
message Increment {
    amount: Int as uint32;
}

// Сообщение с несколькими полями
message TokenTransfer {
    queryId: Int as uint64;
    amount: Int as coins;
    destination: Address;
    responseDestination: Address;
    forwardPayload: Cell;
}

// Сообщение с опциональными полями
message SetConfig {
    key: Int as uint32;
    value: Int?;            // ? означает опциональное поле
}

Отправка сообщений

Функция send()

Основной способ отправки:

send(SendParameters{
    to: destinationAddress,
    value: toNano("1"),       // Количество TON
    mode: SendIgnoreErrors,   // Режим отправки
    bounce: true,             // Вернуть при ошибке
    body: Increment{          // Тело сообщения
        amount: 1,
    }.toCell(),
});

Вспомогательные методы

Tact предоставляет удобные методы для типовых сценариев:

contract Responder {
    init() {}

    receive(msg: Request) {
        // self.reply -- ответить отправителю
        self.reply(Response{
            result: 42,
        }.toCell());
    }

    receive("forward") {
        // self.forward -- переслать другому контракту
        self.forward(
            otherAddress,
            "forwarded message".asComment(),
            true,                  // bounce
            null                   // init (не нужен)
        );
    }
}

Режимы отправки (Send Modes)

Режим определяет, как контракт обрабатывает TON при отправке:

РежимКонстантаОписание
0Обычная отправка, указанная сумма
1SendPayGasSeparatelyГаз оплачивается отдельно от value
2SendIgnoreErrorsИгнорировать ошибки отправки
64SendRemainingValueПереслать оставшийся баланс сообщения
128SendRemainingBalanceОтправить весь баланс контракта

Режимы можно комбинировать:

// Переслать весь остаток + игнорировать ошибки
send(SendParameters{
    to: recipient,
    value: 0,
    mode: SendRemainingValue | SendIgnoreErrors,
    bounce: false,
    body: emptyCell(),
});

Когда какой режим использовать?

// Обычный перевод фиксированной суммы
mode: 0                      // value = точная сумма

// Прокси-контракт: переслать всё, что пришло
mode: SendRemainingValue     // value = 0, пересылает остаток

// Самоуничтожение контракта
mode: SendRemainingBalance   // отправить весь баланс

Практический пример: токен-перевод с bounce

Рассмотрим реалистичный сценарий: контракт отправляет токены, а получатель может отказать:

message TokenSend {
    amount: Int as coins;
    recipient: Address;
}

message TokenReceive {
    amount: Int as coins;
    sender: Address;
}

contract TokenWallet with Deployable {
    balance: Int as coins;
    owner: Address;
    pendingAmount: Int as coins;
    pendingSender: Address;

    init(owner: Address) {
        self.owner = owner;
        self.balance = 1000;
        self.pendingAmount = 0;
        self.pendingSender = newAddress(0, 0);
    }

    receive(msg: TokenSend) {
        require(sender() == self.owner, "Only owner");
        require(self.balance >= msg.amount, "Not enough tokens");

        // Сохраняем pending-данные
        self.pendingAmount = msg.amount;
        self.pendingSender = sender();

        // Списываем баланс
        self.balance -= msg.amount;

        // Отправляем получателю (bounce = true!)
        send(SendParameters{
            to: msg.recipient,
            value: toNano("0.05"),
            mode: SendPayGasSeparately,
            bounce: true,
            body: TokenReceive{
                amount: msg.amount,
                sender: self.owner,
            }.toCell(),
        });
    }

    // Если получатель отверг -- восстанавливаем баланс
    bounced(msg: bounced<TokenReceive>) {
        self.balance += self.pendingAmount;
        self.pendingAmount = 0;
    }

    get fun balance(): Int {
        return self.balance;
    }
}

Что происходит при bounce:

1. TokenWallet отправляет TokenReceive получателю
2. Получатель не может обработать (ошибка, нет receiver)
3. TON создаёт bounced-сообщение и отправляет назад
4. TokenWallet получает bounced<TokenReceive>
5. Баланс восстанавливается
NOTE

Bounced-сообщения содержат только первые 256 бит тела оригинального сообщения. Поэтому самые важные данные (amount, sender) должны быть в начале структуры сообщения.


Флаг bounce

При отправке сообщения поле bounce определяет поведение:

  • bounce: true — если получатель не может обработать, сообщение вернётся
  • bounce: false — сообщение не вернётся (TON будут потеряны при ошибке)
// Безопасная отправка (вернётся при ошибке)
send(SendParameters{
    to: unknown_address,
    value: toNano("1"),
    bounce: true,       // Рекомендуется для переводов
    body: emptyCell(),
});

// Небезопасная отправка (не вернётся)
send(SendParameters{
    to: known_address,
    value: toNano("0.01"),
    bounce: false,      // Только если уверены, что получатель примет
    body: "notification".asComment(),
});

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

  1. Не обрабатывают bounced-сообщения в receive(bounced msg: …): если перевод не удался, контракт не узнает об этом и не откатит своё состояние.
  2. Устанавливают флаг non-bounceable при отправке сообщений с TON, из-за чего при ошибке получателя средства будут потеряны без возможности возврата.
  3. Забывают, что bounced-сообщение содержит только первые 256 бит тела оригинального сообщения, и полные данные оригинала недоступны для recovery-логики.
  4. Не тестируют сценарий bounce в sandbox: не отправляют сообщение несуществующему контракту и не проверяют, что состояние корректно восстановлено.

Проверка знанийKnowledge check
Что произойдёт, если контракт отправит сообщение с bounce: true, а получатель не сможет его обработать, но у отправителя нет bounced receiver?
ОтветAnswer
TON создаст bounced-сообщение и отправит его назад отправителю. Однако без bounced receiver контракт-отправитель не сможет обработать возвращённое сообщение. Это означает, что состояние отправителя не будет восстановлено -- например, если баланс уже был списан, он не вернётся. Отправленные TON вернутся на баланс контракта, но логическое состояние (переменные) останется некорректным.

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

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

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

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