Исполнение на основе сообщений
В этом разделе рассмотрено, как в TON сообщения инициируют транзакции. Вы узнаете, как аккаунты общаются с помощью сообщений, чем различаются типы сообщений, а также как конструировать и отправлять сообщения, чтобы запустить исполнение кода в блокчейне.
Сообщения
В блокчейне TON сообщение — это основная единица взаимодействия аккаунтов (смарт-контрактов). Все действия, изменения состояния и выполнение логики в аккаунтах инициируются сообщениями.
Транзакции и сообщения на жизненном примере
Давайте разберём, как работают транзакции и сообщения в TON, при помощи аналогии. Представьте себе блокчейн TON как уникальный город в глобальном мире — интернете. В этом городе строгие правила: жители никогда не встречаются лично, и единственный способ общения — почта. Люди получают сообщения с просьбой выполнить задание, выполняют его у себя дома, вдали от чужих глаз, а затем отправляют результат другим.
Каждый человек при проверке своего почтового ящика заби рает ровно одно входящее сообщение, затем запирается дома и не выходит, пока задание не будет выполнено. Пока они работают, могут приходить новые письма, но во время работы они полностью их игнорируют.
Если посреди выполнения задания они понимают, что не хватает какой-то необходимой информации, они не могут сделать паузу для уточнений. В этом случае они должны объявить попытку выполнения провалившейся, и, в лучшем случае, вернуть остатки средств с первоначальными инструкциями.
Если задание выполнено успешно, то человек в соответствии с указаниями исходного сообщения:
- отправляет новые сообщения другим жителям, или
- надёжно сохраняет полученный результат у себя дома.
После завершения задачи человек возвращается к своему почтовому ящику и забирает следующее входящее сообщение.
Теперь представьте, что в ходе каждой рабочей сессии человек ведёт подробный журнал. Чтобы рабочие задачи в нём были организованы и разделены, каждая запись в журнале называется транзакцией. Каждая транзакция содержит:
- полное содержание входящего сооб щения (задание),
- заметку о том, что было создано и где это хранится,
- и, опционально, информацию о любых сообщениях, отправленных другим.
Сообщения, доставляемые почтой между жителями, называются внутренними сообщениями (internal messages). В город также поступают сообщения из внешнего мира. У них указан получатель, но отправитель неизвестен. Их называют внешними входящими (external incoming) или сообщениями извне (external-in messages).
Хотя с точки зрения актора внешние и внутренние сообщения выполняют схожую роль, он может обрабатывать их по-разному. Некоторые акторы могут обрабатывать всё, в то время как другие могут полностью игнорировать сообщения из внешнего мира — словно человек, отказывающийся открывать письма от незнакомцев.
Структура сообщения: TL-B
Теперь давайте посмотрим на данные, содержащиеся в сообщениях. В TON различаются внешние и внутренние сообщения, однако оба этих типа способны переносить данные.
В TON все данные представлены с использованием ячеек. Для сериализации данных в ячейку используется устоявшийся стандарт под названием TL-B (Type Language – Binary).
Нативная структура сообщения
В TON все сообщения соответствуют единой схеме, которая определяет их структуру и сериализацию.
message$_ {X:Type} info:CommonMsgInfo
init:(Maybe (Either StateInit ^StateInit))
body:(Either X ^X) = Message X;
info: CommonMsgInfo— содержит метаданные о сообщении.init: (Maybe (Either StateInit ^StateInit))— необязательное поле, используемое для инициализации но вого аккаунта или обновления существующего.body: (Either X ^X)— основное содержание сообщения; может быть либо встроено напрямую в ячейку, либо представлено ссылкой на другую ячейку.
Типы сообщений
В TON существуют три типа сообщений:
- Внешнее входящее: отправлено извне блокчейна → получено смарт-контрактом
- Внутреннее: отправлено смарт-контрактом → получено смарт-контрактом
- Внешнее исходящее: отправлено смарт-контрактом → получено вне блокчейна (неизвестным актором)
Внешние входящие сообщения
Функциональная роль
Внешние входящие (external incoming) сообщения, они же сообщения извне (external-in), служат внешнему миру основной точкой входа для взаимодействия с блокчейном TON. Любой пользователь может отправить произвольные данные любому смарт-контракту, и дальше от контракта зависит, как они будут обработаны. Технически любой аккаунт может получать внешние сообщения. Однако то, как конкретный контракт обрабатывает их, полностью зависит от его внутренней логики. Самый распространённый тип контрактов, поддерживающих работу с такими сообщениями — контракт кошелька.
Среди типичных источников внешних входящих сообщений есть такие:
- пользователи кошельков
- валидаторы
- dApp-сервисы
Хотя внешние входящие сообщения редко используются в основной логике смарт-контрактов, они необходимы для интеграции внешних действий в блокчейн. Любое взаимодействие, которое исходит извне TON — например, отправка TON или использование DEX — начинается с внешнего входящего сообщения. Самый распространённый пример этого — контракт кошелька, который получает инструкцию от пользователя, а затем передаёт её, отправляя внутренние сообщения другим контрактам.
Отправка внешнего входящего сообщения
Термин сообщение тесно связан с транзакцией, но у них разные цели и их не следует путать:
- Сообщения — это пакеты данных с инструкциями для действий, которыми обмениваются смарт-контракты.
- Транзакция — это результат исполнения смарт-контракта в ответ на входящее сообщение. Во время выполнения контракт может обновить своё состояние и создать одно или несколько исходящих сообщений.
Внешнее входящее сообщение — это такое сообщение, чей заголовок CommonMsgInfo использует структуру ext_in_msg_info$10.
message$_ {X:Type} info:CommonMsgInfo
init:(Maybe (Either StateInit ^StateInit))
body:(Either X ^X) = Message X;
TL-B:
//external incoming message
ext_in_msg_info$10 src:MsgAddressExt dest:MsgAddressInt
import_fee:Grams = CommonMsgInfo;
Поскольку внешнее входящее сообщение приходит в блокчейн извне, инициатором является внешний актор. Это может быть либо контракт кошелька, либо специализированный код, взаимодействующий с API-сервисами блокчейна.
Чтобы отправить внешнее входящее сообщение смарт-контракту, вам необходимо:
- Сконструировать структуру данных, соответствующую схеме TL-B
- Отправить эту структуру в блокчейн с помощью сервиса API
Например, в Blueprint есть встроенный помощник, кот орый динамически собирает эту структуру для разработчика.
//@ton/blueprint 0.36.1
import { Address, beginCell } from '@ton/ton';
import { NetworkProvider } from '@ton/blueprint';
export async function run(provider: NetworkProvider, args: string[]) {
//Mainnet address : 'EQAyVZ2rDnEDliuaQJ3PJFKiqAS-9fOm9s7DG1y5Ta16zwU2'
const address = Address.parse('EQAyVZ2rDnEDliuaQJ3PJFKiqAS-9fOm9s7DG1y5Ta16zwU2');
//Switch address for the Testnet :
//const address = Address.parse('kQAyVZ2rDnEDliuaQJ3PJFKiqAS-9fOm9s7DG1y5Ta16z768');
// Get current seqno using blueprint's provider
const contractProvider = provider.provider(address);
const result = await contractProvider.get('seqno', []);
const currentSeqno = result.stack.readNumber();
// Send external message with current seqno
return contractProvider.external(beginCell().storeUint(currentSeqno, 32).endCell());
}
- Подготовьте приложение-кошелёк (например, Tonkeeper), которое вы бу дете использовать для отправки внешнего сообщения
- Создайте локально проект с помощью Blueprint:
npm create ton@latest - Добавьте предоставленный скрипт в ваш проект Blueprint в каталог
scripts, например, так:blueprintproject/scripts/yourscript.ts - Запустите скрипт следующей командой:
npx blueprint run yourscript
Внутренние сообщения
Функциональная роль
Внутреннее сообщение в блокчейне TON — это сообщение, отправленное одним смарт-контрактом другому. Когда контракт получает внутреннее сообщение, он может надёжно определить:
- сколько монет TON содержится в сообщении
- какие контракты являются отправителем и получателем
Блокчейн обеспечивает целостность этой информации, позволяя доверять контракту и безопасно его использовать. TL-B:
Внутренние сообщения всегда инициированы контрактом — они являются результатом выполнения транзакции. Другими словами, как следует из названия, отправителем внутреннего сообщения всегда является смарт-контракт.
Самый распространённый способ отправки внутреннего сообщения — сначала отправить внешнее сообщение контракту, который содержит логику для пересылки внутреннего сообщения другому контракту.
//internal message
int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
src:MsgAddressInt dest:MsgAddressInt
value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams
created_lt:uint64 created_at:uint32 = CommonMsgInfo;
| Структура | Описание |
|---|---|
int_msg_info$0 | Указывает, что сообщение внутреннее. Тег $0 означает, что CommonMsgInfo начинается с бита 0. |
ihr_disabled | Флаг, указывающий, отключена ли маршрутизация Instant Hypercube Routing (IHR). |
bounce | Если установлено значение 1, при ошибке обработки сообщение будет возвращено. |
bounced | Флаг, указывающий, является ли само сообщение результатом такого возврата. |
src | Адрес смарт-контракта, отправившего сообщение. |
dest | Адрес смарт-контракта, которому предназначено сообщение. |
value | Структура, описывающая сумму и тип средств, переданных в сообщении. |
ihr_fee | Комиссия за доставку через IHR. |
fwd_fee | Комиссия за пересылку сообщения. |
created_lt | Логическое время создания сообщения, используется для упорядочивания действий контракта. |
created_at | Время создания сообщения в формате временной метки Unix. |
В первую очередь разработчику требуется указать value (сумму Toncoin, прикреплённую к внутреннему сообщению) и dest (адрес назначения).
В этом примере контракту кошелька отправля ют внешнее сообщение с дополнительными инструкциями по отправке внутреннего сообщения. В результате обработки транзакции контракт кошелька отправляет исходящее внутреннее сообщение с указанными значениями value и адресом получателя address.
//@ton/blueprint 0.36.1
import { Address } from '@ton/ton';
import { NetworkProvider } from '@ton/blueprint';
export async function run(provider: NetworkProvider, args: string[]) {
const address = Address.parse('kQBUCuxgGsF6znHM_yNmnV_EwtlmdvmDzqTxiWHJip2ux6Wn');
const contractProvider = provider.provider(address);
return contractProvider.internal(provider.sender(), {
value: '0.01',
});
}