Сообщения и 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;
}
}
Не обрабатывать 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 | — | Обычная отправка, указанная сумма |
| 1 | SendPayGasSeparately | Газ оплачивается отдельно от value |
| 2 | SendIgnoreErrors | Игнорировать ошибки отправки |
| 64 | SendRemainingValue | Переслать оставшийся баланс сообщения |
| 128 | SendRemainingBalance | Отправить весь баланс контракта |
Режимы можно комбинировать:
// Переслать весь остаток + игнорировать ошибки
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. Баланс восстанавливается
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(),
});
Частые ошибки
- Не обрабатывают bounced-сообщения в receive(bounced msg: …): если перевод не удался, контракт не узнает об этом и не откатит своё состояние.
- Устанавливают флаг non-bounceable при отправке сообщений с TON, из-за чего при ошибке получателя средства будут потеряны без возможности возврата.
- Забывают, что bounced-сообщение содержит только первые 256 бит тела оригинального сообщения, и полные данные оригинала недоступны для recovery-логики.
- Не тестируют сценарий bounce в sandbox: не отправляют сообщение несуществующему контракту и не проверяют, что состояние корректно восстановлено.
Проверка знанийЧто произойдёт, если контракт отправит сообщение с bounce: true, а получатель не сможет его обработать, но у отправителя нет bounced receiver?
Check Your Understanding
Finished the lesson?
Mark it as complete to track your progress
Войдите чтобы оценить урок