Исполнение на основе сообщений
В этом разделе рассмотрено, как в 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',
});
}
Внешние исходящие сообщения
Внешние исходящие сообщения (также известные как логи) — особый тип сообщений, генерируемых смарт-контрактами. Они не адресованы какому-либо конкретному получателю. Вместо этого они служат логами или сигналами, которые можно отслеживать вне блокчейна. Хотя эти сообщения редко используются в основной логике смарт-контрактов, они важны для таких задач, как:
- индексация и мониторинг состояния блокчейна
- запуск процессов вне блокчейна, вроде уведомления внешних служб или обновления пользовательского интерфейса
Например, контракт децентрализованной биржи (DEX) может выпустить внешнее исходящее сообщение после успешного обмена токенов, чтобы уведомить внешние системы или пользователей о событии.
TL-B:
//external outgoing message
ext_out_msg_info$11 src:MsgAddressInt dest:MsgAddressExt
created_lt:uint64 created_at:uint32 = CommonMsgInfo;
Возврат сообщений
Возвратное сообщение (bounce message) автоматически отправляется обратно отправителю, если транзакция завершается неудачей: например, смарт-контракт получателя не существует или его выполнение завершается ошибкой. Этот механизм не только сигнализирует о сбое, но и позволяет отправителю отреагировать соответствующим образом, например, вернув токены или отобразив пользователю сообщение об ошибке.
Большинство внутренних сообщений между смарт-контрактами должны быть возвращаемыми (то есть у них должен быть установлен флаг bounce), чтобы в случае сбоя:
- сообщение возвращалось отправителю
- любые оставшиеся средства после вычета комиссий возвращались
Смарт- контракты, в свою очередь, должны:
- проверять флаг bounced во входящих сообщениях
- и либо игнорировать возвращённые сообщения (завершать исполнение с
code = 0
) - либо явно обрабатывать ошибку
Вы можете запустить в Blueprint приведённый далее скрипт, чтобы визуально сравнить различия между возвращаемыми и невозвращаемыми сообщениями.
Используйте один из следующих способов для запуска скрипта:
Вариант 1: с помощью переменных окружения в командной строке
WALLET_MNEMONIC="unfold your mnemonics ... is added here" \
WALLET_VERSION="v4r2" \
npx blueprint run sendBounceableMessages --testnet --mnemonic
Вариант 2: с помощью файла .env
Создайте файл .env
в исходном каталоге:
WALLET_MNEMONIC="unfold your mnemonics ... is added here"
WALLET_VERSION="v4r2"
Затем запустите:
npx blueprint run sendBounceableMessages --testnet --mnemonic
Примечание: В данном случае невозможно использовать TON Connect (например, с помощью флага --tonconnect
), поскольку кошельки вроде Tonkeeper ради безопасности не позволяют dApp-приложениям контролировать флаг bounce
. Они всегда отправляют возвращаемые сообщения. Чтобы сохранить этот флаг, вам необходимо создать и подписать сообщение вручную, используя мнемоническую фразу.
Отправить возвращаемо е сообщение
import { Address, toNano, beginCell, internal } from '@ton/core';
import { NetworkProvider } from '@ton/blueprint';
import { mnemonicToWalletKey } from '@ton/crypto';
import { WalletContractV3R2, WalletContractV4 } from '@ton/ton';
export async function run(provider: NetworkProvider): Promise<void> {
// --- Get wallet data from environment variables ---
const mnemonic = process.env.WALLET_MNEMONIC;
const versionStr = process.env.WALLET_VERSION || 'v4r2';
if (!mnemonic) {
throw new Error('WALLET_MNEMONIC environment variable is not set. Please see instructions in the script file.');
}
// --- Initialize wallet from mnemonic ---
const keyPair = await mnemonicToWalletKey(mnemonic.split(' '));
const secretKey = keyPair.secretKey;
let wallet;
if (versionStr === 'v3r1' || versionStr === 'v3r2') {
wallet = WalletContractV3R2.create({ workchain: 0, publicKey: keyPair.publicKey });
} else if (versionStr === 'v4r1' || versionStr === 'v4r2') {
wallet = WalletContractV4.create({ workchain: 0, publicKey: keyPair.publicKey });
} else {
throw new Error(
'Unsupported wallet version: ' + versionStr + '. Supported versions are v3r1, v3r2, v4r1, v4r2.',
);
}
const myAddress = wallet.address;
console.log(`Using wallet ${versionStr} at address: ${myAddress.toString()}`);
// --- Check if wallet is deployed ---
if (!(await provider.isContractDeployed(myAddress))) {
console.log('Wallet is not deployed. Please send some TON to this address and try again.');
return;
}
// This is a valid address, but its purpose is for burning.
// 0:0000000000000000000000000000000000000000000000000000000000000000
// https://ton.org/address/
// We will send messages to it to demonstrate bounce handling.
const burnAddress = Address.parse('EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c');
// --- Get current seqno ---
const walletProvider = provider.provider(myAddress);
let seqno = await wallet.getSeqno(walletProvider);
console.log(` Current seqno: ${seqno}`);
// --- Create messages ---
// This message is sent to a burn address, but it is marked as "bounceable".
// The transaction will fail on the recipient's side (as it's just a blackhole),
// and the value (minus fees) will be returned to the sender.
const bounceableMessage = internal({
to: burnAddress,
value: toNano('0.005'),
body: beginCell().storeUint(0, 32).storeStringTail('bounceable test').endCell(),
bounce: true,
});
// This message is also sent to a burn address, but it is marked as "non-bounceable".
// The value sent with this message will be lost forever.
const nonBounceableMessage = internal({
to: burnAddress,
value: toNano('0.005'),
body: beginCell().storeUint(0, 32).storeStringTail('non-bounceable test').endCell(),
bounce: false,
});
// --- Create a transfer with both messages ---
console.log('Sending one external message with two internal messages (one bounceable, one non-bounceable)...');
const transfer = wallet.createTransfer({
seqno: seqno,
secretKey: secretKey,
messages: [bounceableMessage, nonBounceableMessage], // Send both in one transaction
});
await walletProvider.external(transfer);
console.log('External message sent successfully!');
}
Ожидаемый вывод в консоли
Using file: sendBounceableMessages
Connected to wallet at address: 0QD3oTH51Tp4UNhCLfX3axyG2X9H_9wZpLoC7W--WbXwxYkp
Using wallet v4r2 at address: EQD3oTH51Tp4UNhCLfX3axyG2X9H_9wZpLoC7W--WbXwxW9m
Current seqno: 36
Sending one external message with two internal messages (one bounceable, one non-bounceable)...
External message sent successfully!
Содержимое сообщения при возврате не должно выполняться как обычный запрос; оно предназначено только для сигнализации об ошибке и возврата средств.